feat: run transaction hooks at boundaries and add configurable LTO flags
- execute transaction pre hooks at transaction start and post hooks at transaction end - add batched hook execution with ordered "(n/total)" logging - refactor install flows to plan package actions first, then run hooks around commits - add `build.flags.ltoflags` and `build.flags.use_lto` (default: true) - export `LTOFLAGS` and append LTO flags to `CFLAGS`, `CXXFLAGS`, and `LDFLAGS` unless `use_lto = false` - make autotools CFLAGS expansion use the final computed CFLAGS value - update spec parsing/appends, interactive TOML output, docs, and contrib build config - bump project version to 0.8.0 and update `zip` to 8.2.0
This commit is contained in:
Generated
+3
-3
@@ -382,7 +382,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
|
||||
|
||||
[[package]]
|
||||
name = "depot"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ar",
|
||||
@@ -2995,9 +2995,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "8.1.0"
|
||||
version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e499faf5c6b97a0d086f4a8733de6d47aee2252b8127962439d8d4311a73f72"
|
||||
checksum = "b680f2a0cd479b4cff6e1233c483fdead418106eae419dc60200ae9850f6d004"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bzip2",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "depot"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
edition = "2024"
|
||||
|
||||
[lints.rust]
|
||||
@@ -27,7 +27,7 @@ toml = "0.9.8"
|
||||
url = "2.5.8"
|
||||
walkdir = "2.5.0"
|
||||
xz2 = "0.1.7"
|
||||
zip = "8.1.0"
|
||||
zip = "8.2.0"
|
||||
zstd = { version = "0.13.3", features = ["zstdmt"] }
|
||||
inquire = "0.9.4"
|
||||
md5 = "0.8.0"
|
||||
|
||||
@@ -85,6 +85,10 @@ runtime = ["libc"]
|
||||
optional = ["bash-completion"]
|
||||
```
|
||||
|
||||
LTO controls are available via `build.flags`:
|
||||
- `ltoflags`: exported as `LTOFLAGS`
|
||||
- `use_lto`: defaults to `true`; when enabled, `ltoflags` are appended to `CFLAGS`, `CXXFLAGS`, and `LDFLAGS`
|
||||
|
||||
## Configuration
|
||||
|
||||
Depot can be configured via `/etc/depot.d/` (or relative to the rootfs).
|
||||
|
||||
@@ -10,6 +10,10 @@ cflags += ["-pipe"]
|
||||
# Default LDFLAGS
|
||||
ldflags = ["-Wl,-O1"]
|
||||
|
||||
# Default LTOFLAGS and automatic injection into CFLAGS/CXXFLAGS/LDFLAGS.
|
||||
ltoflags = ["-flto=auto"]
|
||||
use_lto = true
|
||||
|
||||
# CARCH short name (can be overridden per-package)
|
||||
carch = "x86_64"
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
project(
|
||||
'depot',
|
||||
version: '0.7.0',
|
||||
version: '0.8.0',
|
||||
meson_version: '>=0.60.0',
|
||||
)
|
||||
|
||||
|
||||
@@ -32,9 +32,14 @@ pub fn build(
|
||||
flags.cc.clone()
|
||||
};
|
||||
|
||||
if export_compiler_flags && !flags.cflags.is_empty() {
|
||||
// Expand shell command substitutions like $($CC -print-resource-dir)
|
||||
let cflags_str = flags.cflags.join(" ");
|
||||
if export_compiler_flags
|
||||
&& let Some(cflags_str) = env_vars
|
||||
.iter()
|
||||
.find(|(key, _)| key == "CFLAGS")
|
||||
.map(|(_, value)| value.clone())
|
||||
&& !cflags_str.trim().is_empty()
|
||||
{
|
||||
// Expand shell command substitutions like $($CC -print-resource-dir).
|
||||
let expanded = expand_shell_commands(&cflags_str, &cc)?;
|
||||
crate::builder::set_env_var(&mut env_vars, "CFLAGS", expanded);
|
||||
}
|
||||
|
||||
+89
-12
@@ -28,6 +28,23 @@ pub fn set_env_var(env_vars: &mut EnvVars, key: &str, value: impl Into<String>)
|
||||
}
|
||||
}
|
||||
|
||||
fn compiler_flag_sets(
|
||||
flags: &crate::package::BuildFlags,
|
||||
) -> (Vec<String>, Vec<String>, Vec<String>, Vec<String>) {
|
||||
let mut cflags = flags.cflags.clone();
|
||||
let mut cxxflags = flags.cxxflags.clone();
|
||||
let mut ldflags = flags.ldflags.clone();
|
||||
let ltoflags = flags.ltoflags.clone();
|
||||
|
||||
if flags.use_lto && !ltoflags.is_empty() {
|
||||
cflags.extend(ltoflags.iter().cloned());
|
||||
cxxflags.extend(ltoflags.iter().cloned());
|
||||
ldflags.extend(ltoflags.iter().cloned());
|
||||
}
|
||||
|
||||
(cflags, cxxflags, ldflags, ltoflags)
|
||||
}
|
||||
|
||||
pub fn standard_build_env(
|
||||
spec: &PackageSpec,
|
||||
cross: Option<&CrossConfig>,
|
||||
@@ -39,24 +56,25 @@ pub fn standard_build_env(
|
||||
let export_compiler_flags = export_compiler_flags && !flags.no_flags;
|
||||
|
||||
if include_compiler_env && export_compiler_flags {
|
||||
if !flags.cflags.is_empty() {
|
||||
set_env_var(&mut env_vars, "CFLAGS", flags.cflags.join(" "));
|
||||
let (cflags, cxxflags, ldflags, ltoflags) = compiler_flag_sets(flags);
|
||||
|
||||
if !cflags.is_empty() {
|
||||
set_env_var(&mut env_vars, "CFLAGS", cflags.join(" "));
|
||||
}
|
||||
if !flags.cxxflags.is_empty() {
|
||||
set_env_var(&mut env_vars, "CXXFLAGS", flags.cxxflags.join(" "));
|
||||
if !cxxflags.is_empty() {
|
||||
set_env_var(&mut env_vars, "CXXFLAGS", cxxflags.join(" "));
|
||||
}
|
||||
if !ltoflags.is_empty() {
|
||||
set_env_var(&mut env_vars, "LTOFLAGS", ltoflags.join(" "));
|
||||
}
|
||||
|
||||
let ldflags = if !flags.ldflags.is_empty() || !flags.libc.is_empty() {
|
||||
let ldflags = if !ldflags.is_empty() || !flags.libc.is_empty() {
|
||||
if flags.libc.is_empty() {
|
||||
flags.ldflags.join(" ")
|
||||
} else if flags.ldflags.is_empty() {
|
||||
ldflags.join(" ")
|
||||
} else if ldflags.is_empty() {
|
||||
format!("-Wl,--dynamic-linker={}", flags.libc)
|
||||
} else {
|
||||
format!(
|
||||
"{} -Wl,--dynamic-linker={}",
|
||||
flags.ldflags.join(" "),
|
||||
flags.libc
|
||||
)
|
||||
format!("{} -Wl,--dynamic-linker={}", ldflags.join(" "), flags.libc)
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
@@ -424,6 +442,65 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_standard_build_env_injects_ltoflags_into_compiler_and_linker_flags() {
|
||||
let mut spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]);
|
||||
spec.build.flags.cxxflags = vec!["-O2".into()];
|
||||
spec.build.flags.ltoflags = vec!["-flto=auto".into(), "-fuse-linker-plugin".into()];
|
||||
spec.build.flags.use_lto = true;
|
||||
|
||||
let env = standard_build_env(&spec, None, true, true);
|
||||
assert!(
|
||||
env.iter()
|
||||
.any(|(k, v)| { k == "CFLAGS" && v == "-O2 -flto=auto -fuse-linker-plugin" }),
|
||||
"expected LTOFLAGS to be appended to CFLAGS"
|
||||
);
|
||||
assert!(
|
||||
env.iter()
|
||||
.any(|(k, v)| k == "CXXFLAGS" && v == "-O2 -flto=auto -fuse-linker-plugin"),
|
||||
"expected LTOFLAGS to be appended to CXXFLAGS"
|
||||
);
|
||||
assert!(
|
||||
env.iter().any(|(k, v)| {
|
||||
k == "LDFLAGS" && v == "-Wl,--as-needed -flto=auto -fuse-linker-plugin"
|
||||
}),
|
||||
"expected LTOFLAGS to be appended to LDFLAGS"
|
||||
);
|
||||
assert!(
|
||||
env.iter()
|
||||
.any(|(k, v)| k == "LTOFLAGS" && v == "-flto=auto -fuse-linker-plugin"),
|
||||
"expected LTOFLAGS variable to be exported"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_standard_build_env_skips_lto_injection_when_disabled() {
|
||||
let mut spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]);
|
||||
spec.build.flags.cxxflags = vec!["-O2".into()];
|
||||
spec.build.flags.ltoflags = vec!["-flto=auto".into()];
|
||||
spec.build.flags.use_lto = false;
|
||||
|
||||
let env = standard_build_env(&spec, None, true, true);
|
||||
assert!(
|
||||
env.iter().any(|(k, v)| k == "CFLAGS" && v == "-O2"),
|
||||
"expected CFLAGS to remain unchanged when use_lto is false"
|
||||
);
|
||||
assert!(
|
||||
env.iter().any(|(k, v)| k == "CXXFLAGS" && v == "-O2"),
|
||||
"expected CXXFLAGS to remain unchanged when use_lto is false"
|
||||
);
|
||||
assert!(
|
||||
env.iter()
|
||||
.any(|(k, v)| k == "LDFLAGS" && v == "-Wl,--as-needed"),
|
||||
"expected LDFLAGS to remain unchanged when use_lto is false"
|
||||
);
|
||||
assert!(
|
||||
env.iter()
|
||||
.any(|(k, v)| k == "LTOFLAGS" && v == "-flto=auto"),
|
||||
"expected LTOFLAGS variable to be exported even when use_lto is false"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_standard_build_env_exports_makeflags() {
|
||||
let mut spec = mk_spec(Vec::new(), Vec::new());
|
||||
|
||||
+180
-75
@@ -491,12 +491,25 @@ fn build_lib32_companion_package(
|
||||
Ok(Some((lib32_pkg_spec, lib32_destdir)))
|
||||
}
|
||||
|
||||
fn install_staged_to_rootfs(
|
||||
#[derive(Debug, Clone)]
|
||||
struct PlannedStagedInstall {
|
||||
is_update: bool,
|
||||
remove_paths: Vec<String>,
|
||||
hook_context: install::hooks::HookExecutionContextOwned,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PlannedPackageInstall {
|
||||
spec: package::PackageSpec,
|
||||
destdir: PathBuf,
|
||||
staged: PlannedStagedInstall,
|
||||
}
|
||||
|
||||
fn plan_staged_install(
|
||||
pkg_spec: &package::PackageSpec,
|
||||
destdir: &Path,
|
||||
rootfs: &Path,
|
||||
config: &config::Config,
|
||||
) -> Result<()> {
|
||||
) -> Result<PlannedStagedInstall> {
|
||||
std::fs::create_dir_all(&config.db_dir).with_context(|| {
|
||||
format!(
|
||||
"Failed to create database directory: {}",
|
||||
@@ -506,10 +519,6 @@ fn install_staged_to_rootfs(
|
||||
let db_path = config.db_dir.join("packages.db");
|
||||
|
||||
let is_update = db::get_package_version(&db_path, &pkg_spec.package.name)?.is_some();
|
||||
let staged_scripts_dir = install::scripts::staged_scripts_dir(destdir);
|
||||
let installed_scripts_dir =
|
||||
install::scripts::installed_scripts_dir(rootfs, &pkg_spec.package.name);
|
||||
|
||||
let new_files = staging::generate_manifest_with_dirs(destdir)?;
|
||||
let remove_paths =
|
||||
db::calculate_upgrade_paths(&db_path, &pkg_spec.package.name, &new_files.files)?;
|
||||
@@ -523,17 +532,64 @@ fn install_staged_to_rootfs(
|
||||
affected_paths.sort();
|
||||
affected_paths.dedup();
|
||||
|
||||
install::hooks::run_transaction_hooks(
|
||||
rootfs,
|
||||
&install::hooks::HookExecutionContext {
|
||||
phase: install::hooks::HookPhase::Pre,
|
||||
Ok(PlannedStagedInstall {
|
||||
is_update,
|
||||
remove_paths,
|
||||
hook_context: install::hooks::HookExecutionContextOwned {
|
||||
operation,
|
||||
package: &pkg_spec.package.name,
|
||||
affected_paths: &affected_paths,
|
||||
package: pkg_spec.package.name.clone(),
|
||||
affected_paths,
|
||||
},
|
||||
)?;
|
||||
})
|
||||
}
|
||||
|
||||
if is_update {
|
||||
fn plan_package_outputs_for_install(
|
||||
pkg_spec: &package::PackageSpec,
|
||||
destdir: &Path,
|
||||
config: &config::Config,
|
||||
) -> Result<Vec<PlannedPackageInstall>> {
|
||||
let mut plans = Vec::new();
|
||||
for out in pkg_spec.outputs() {
|
||||
let mut spec_for_out = pkg_spec.clone();
|
||||
let output_name = out.name.clone();
|
||||
spec_for_out.package = out;
|
||||
spec_for_out.alternatives = pkg_spec.alternatives_for_output(&output_name);
|
||||
spec_for_out.dependencies = pkg_spec.dependencies_for_output(&output_name);
|
||||
let out_destdir = output_destdir_for(destdir, &pkg_spec.package.name, &output_name);
|
||||
let staged = plan_staged_install(&spec_for_out, &out_destdir, config)?;
|
||||
plans.push(PlannedPackageInstall {
|
||||
spec: spec_for_out,
|
||||
destdir: out_destdir,
|
||||
staged,
|
||||
});
|
||||
}
|
||||
Ok(plans)
|
||||
}
|
||||
|
||||
fn run_transaction_hooks_for_plans(
|
||||
rootfs: &Path,
|
||||
phase: install::hooks::HookPhase,
|
||||
plans: &[PlannedPackageInstall],
|
||||
) -> Result<usize> {
|
||||
let contexts: Vec<_> = plans
|
||||
.iter()
|
||||
.map(|plan| plan.staged.hook_context.clone())
|
||||
.collect();
|
||||
install::hooks::run_transaction_hooks_batch(rootfs, phase, &contexts)
|
||||
}
|
||||
|
||||
fn install_staged_to_rootfs(
|
||||
pkg_spec: &package::PackageSpec,
|
||||
destdir: &Path,
|
||||
rootfs: &Path,
|
||||
config: &config::Config,
|
||||
plan: &PlannedStagedInstall,
|
||||
) -> Result<()> {
|
||||
let staged_scripts_dir = install::scripts::staged_scripts_dir(destdir);
|
||||
let installed_scripts_dir =
|
||||
install::scripts::installed_scripts_dir(rootfs, &pkg_spec.package.name);
|
||||
|
||||
if plan.is_update {
|
||||
let has_staged_pre = install::scripts::run_hook_if_present(
|
||||
&staged_scripts_dir,
|
||||
install::scripts::Hook::PreUpdate,
|
||||
@@ -562,10 +618,11 @@ fn install_staged_to_rootfs(
|
||||
destdir,
|
||||
rootfs,
|
||||
&tx_base,
|
||||
&remove_paths,
|
||||
&plan.remove_paths,
|
||||
&pkg_spec.build.flags.keep,
|
||||
)?;
|
||||
|
||||
let db_path = config.db_dir.join("packages.db");
|
||||
if let Err(e) = db::register_package(&db_path, pkg_spec, destdir) {
|
||||
let _ = tx.rollback();
|
||||
return Err(e);
|
||||
@@ -578,7 +635,7 @@ fn install_staged_to_rootfs(
|
||||
&pkg_spec.package.name,
|
||||
)?;
|
||||
|
||||
if is_update {
|
||||
if plan.is_update {
|
||||
let _ = install::scripts::run_hook_if_present(
|
||||
&installed_scripts_dir,
|
||||
install::scripts::Hook::PostUpdate,
|
||||
@@ -594,36 +651,33 @@ fn install_staged_to_rootfs(
|
||||
)?;
|
||||
}
|
||||
|
||||
install::hooks::run_transaction_hooks(
|
||||
rootfs,
|
||||
&install::hooks::HookExecutionContext {
|
||||
phase: install::hooks::HookPhase::Post,
|
||||
operation,
|
||||
package: &pkg_spec.package.name,
|
||||
affected_paths: &affected_paths,
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_planned_packages_to_rootfs(
|
||||
plans: &[PlannedPackageInstall],
|
||||
rootfs: &Path,
|
||||
config: &config::Config,
|
||||
) -> Result<Vec<package::PackageInfo>> {
|
||||
let mut installed = Vec::with_capacity(plans.len());
|
||||
for plan in plans {
|
||||
install_staged_to_rootfs(&plan.spec, &plan.destdir, rootfs, config, &plan.staged)?;
|
||||
installed.push(plan.spec.package.clone());
|
||||
}
|
||||
Ok(installed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn install_package_outputs_to_rootfs(
|
||||
pkg_spec: &package::PackageSpec,
|
||||
destdir: &Path,
|
||||
rootfs: &Path,
|
||||
config: &config::Config,
|
||||
) -> Result<Vec<package::PackageInfo>> {
|
||||
let mut installed = Vec::new();
|
||||
for out in pkg_spec.outputs() {
|
||||
let mut spec_for_out = pkg_spec.clone();
|
||||
let output_name = out.name.clone();
|
||||
spec_for_out.package = out;
|
||||
spec_for_out.alternatives = pkg_spec.alternatives_for_output(&output_name);
|
||||
spec_for_out.dependencies = pkg_spec.dependencies_for_output(&output_name);
|
||||
let out_destdir = output_destdir_for(destdir, &pkg_spec.package.name, &output_name);
|
||||
install_staged_to_rootfs(&spec_for_out, &out_destdir, rootfs, config)?;
|
||||
installed.push(spec_for_out.package);
|
||||
}
|
||||
let plans = plan_package_outputs_for_install(pkg_spec, destdir, config)?;
|
||||
run_transaction_hooks_for_plans(rootfs, install::hooks::HookPhase::Pre, &plans)?;
|
||||
let installed = install_planned_packages_to_rootfs(&plans, rootfs, config)?;
|
||||
run_transaction_hooks_for_plans(rootfs, install::hooks::HookPhase::Post, &plans)?;
|
||||
Ok(installed)
|
||||
}
|
||||
|
||||
@@ -1196,8 +1250,32 @@ fn execute_install_plan_with_child_commands(
|
||||
signature_pb.finish_and_clear();
|
||||
}
|
||||
|
||||
let mut binary_pre_hook_plans = Vec::new();
|
||||
for step in &actionable_steps {
|
||||
if let planner::PlanOrigin::Binary { repo_name, record } = &step.origin {
|
||||
let cached = binary_archives
|
||||
.get(&(repo_name.clone(), record.filename.clone()))
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Cached archive missing for planned binary step '{}' from repo '{}'",
|
||||
record.filename, repo_name
|
||||
)
|
||||
})?;
|
||||
let staged = extract_package_archive_to_staging(&cached.package_path)?;
|
||||
let spec = package_spec_from_repo_record(record);
|
||||
let plans = plan_package_outputs_for_install(&spec, staged.path(), config)?;
|
||||
binary_pre_hook_plans.extend(plans);
|
||||
}
|
||||
}
|
||||
run_transaction_hooks_for_plans(
|
||||
rootfs,
|
||||
install::hooks::HookPhase::Pre,
|
||||
&binary_pre_hook_plans,
|
||||
)?;
|
||||
|
||||
let exe = std::env::current_exe().context("Failed to locate depot executable")?;
|
||||
let total_steps = actionable_steps.len();
|
||||
let mut binary_post_hook_plans = Vec::new();
|
||||
for (idx, step) in actionable_steps.into_iter().enumerate() {
|
||||
match &step.origin {
|
||||
planner::PlanOrigin::Source { path, .. } => {
|
||||
@@ -1241,8 +1319,9 @@ fn execute_install_plan_with_child_commands(
|
||||
})?;
|
||||
let staged = extract_package_archive_to_staging(&cached.package_path)?;
|
||||
let spec = package_spec_from_repo_record(record);
|
||||
let installed =
|
||||
install_package_outputs_to_rootfs(&spec, staged.path(), rootfs, config)?;
|
||||
let plans = plan_package_outputs_for_install(&spec, staged.path(), config)?;
|
||||
let installed = install_planned_packages_to_rootfs(&plans, rootfs, config)?;
|
||||
binary_post_hook_plans.extend(plans);
|
||||
for pkg in installed {
|
||||
ui::success(format!("Installed {} v{}", pkg.name, pkg.version));
|
||||
}
|
||||
@@ -1251,6 +1330,11 @@ fn execute_install_plan_with_child_commands(
|
||||
}
|
||||
}
|
||||
|
||||
run_transaction_hooks_for_plans(
|
||||
rootfs,
|
||||
install::hooks::HookPhase::Post,
|
||||
&binary_post_hook_plans,
|
||||
)?;
|
||||
install::scripts::run_deferred_hooks_if_possible(rootfs)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1528,6 +1612,8 @@ fn run_direct_install_request(
|
||||
destdir
|
||||
};
|
||||
|
||||
let mut transaction_plans = Vec::new();
|
||||
|
||||
if !options.lib32_only {
|
||||
if staging_dir.is_none() {
|
||||
// Source-build path: apply staging transforms (strip/compress/static cleanup).
|
||||
@@ -1537,15 +1623,8 @@ fn run_direct_install_request(
|
||||
ui::info("Installing binary archive payload without staging transforms");
|
||||
}
|
||||
|
||||
let installed =
|
||||
install_package_outputs_to_rootfs(&pkg_spec, &destdir, options.rootfs, config)?;
|
||||
for pkg in installed {
|
||||
ui::success(format!(
|
||||
"Successfully installed {} v{}",
|
||||
pkg.name, pkg.version
|
||||
));
|
||||
// TODO(snapper): create post-install snapshot after install commit succeeds.
|
||||
}
|
||||
let output_plans = plan_package_outputs_for_install(&pkg_spec, &destdir, config)?;
|
||||
transaction_plans.extend(output_plans);
|
||||
}
|
||||
|
||||
if let Some(src_dir) = built_src_dir.as_deref()
|
||||
@@ -1558,12 +1637,32 @@ fn run_direct_install_request(
|
||||
options.lib32_only,
|
||||
)?
|
||||
{
|
||||
install_staged_to_rootfs(&lib32_spec, &lib32_destdir, options.rootfs, config)?;
|
||||
let staged = plan_staged_install(&lib32_spec, &lib32_destdir, config)?;
|
||||
transaction_plans.push(PlannedPackageInstall {
|
||||
spec: lib32_spec,
|
||||
destdir: lib32_destdir,
|
||||
staged,
|
||||
});
|
||||
}
|
||||
|
||||
run_transaction_hooks_for_plans(
|
||||
options.rootfs,
|
||||
install::hooks::HookPhase::Pre,
|
||||
&transaction_plans,
|
||||
)?;
|
||||
let installed = install_planned_packages_to_rootfs(&transaction_plans, options.rootfs, config)?;
|
||||
for pkg in installed {
|
||||
ui::success(format!(
|
||||
"Successfully installed {} v{}",
|
||||
lib32_spec.package.name, lib32_spec.package.version
|
||||
pkg.name, pkg.version
|
||||
));
|
||||
// TODO(snapper): create post-install snapshot after install commit succeeds.
|
||||
}
|
||||
run_transaction_hooks_for_plans(
|
||||
options.rootfs,
|
||||
install::hooks::HookPhase::Post,
|
||||
&transaction_plans,
|
||||
)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
@@ -1925,37 +2024,43 @@ pub fn run(cli: Cli) -> Result<()> {
|
||||
));
|
||||
}
|
||||
if ui::prompt_package_action("installation", &install_targets, false)? {
|
||||
let mut transaction_plans = Vec::new();
|
||||
if !cli.lib32_only {
|
||||
for out in pkg_spec.outputs() {
|
||||
let mut spec_for_out = pkg_spec.clone();
|
||||
let output_name = out.name.clone();
|
||||
spec_for_out.package = out;
|
||||
spec_for_out.alternatives =
|
||||
pkg_spec.alternatives_for_output(&output_name);
|
||||
spec_for_out.dependencies =
|
||||
pkg_spec.dependencies_for_output(&output_name);
|
||||
let out_destdir =
|
||||
output_destdir_for(&destdir, &pkg_spec.package.name, &output_name);
|
||||
install_staged_to_rootfs(
|
||||
&spec_for_out,
|
||||
&out_destdir,
|
||||
&cli.rootfs,
|
||||
&config,
|
||||
)?;
|
||||
ui::success(format!(
|
||||
"Successfully installed {} v{}",
|
||||
spec_for_out.package.name, spec_for_out.package.version
|
||||
));
|
||||
// TODO(snapper): create post-install snapshot after --install commit succeeds.
|
||||
}
|
||||
let output_plans =
|
||||
plan_package_outputs_for_install(&pkg_spec, &destdir, &config)?;
|
||||
transaction_plans.extend(output_plans);
|
||||
}
|
||||
if let Some((lib32_spec, lib32_destdir)) = &lib32_install_bundle {
|
||||
install_staged_to_rootfs(lib32_spec, lib32_destdir, &cli.rootfs, &config)?;
|
||||
let staged = plan_staged_install(lib32_spec, lib32_destdir, &config)?;
|
||||
transaction_plans.push(PlannedPackageInstall {
|
||||
spec: lib32_spec.clone(),
|
||||
destdir: lib32_destdir.clone(),
|
||||
staged,
|
||||
});
|
||||
}
|
||||
|
||||
run_transaction_hooks_for_plans(
|
||||
&cli.rootfs,
|
||||
install::hooks::HookPhase::Pre,
|
||||
&transaction_plans,
|
||||
)?;
|
||||
let installed = install_planned_packages_to_rootfs(
|
||||
&transaction_plans,
|
||||
&cli.rootfs,
|
||||
&config,
|
||||
)?;
|
||||
for pkg in installed {
|
||||
ui::success(format!(
|
||||
"Successfully installed {} v{}",
|
||||
lib32_spec.package.name, lib32_spec.package.version
|
||||
pkg.name, pkg.version
|
||||
));
|
||||
// TODO(snapper): create post-install snapshot after --install commit succeeds.
|
||||
}
|
||||
run_transaction_hooks_for_plans(
|
||||
&cli.rootfs,
|
||||
install::hooks::HookPhase::Post,
|
||||
&transaction_plans,
|
||||
)?;
|
||||
|
||||
install::scripts::run_deferred_hooks_if_possible(&cli.rootfs)?;
|
||||
} else {
|
||||
|
||||
+145
-14
@@ -104,6 +104,17 @@ pub struct HookExecutionContext<'a> {
|
||||
pub affected_paths: &'a [String],
|
||||
}
|
||||
|
||||
/// Owned transaction hook context used for batched execution.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HookExecutionContextOwned {
|
||||
/// Current operation (`install`, `update`, `remove`).
|
||||
pub operation: HookOperation,
|
||||
/// Package being processed.
|
||||
pub package: String,
|
||||
/// Filesystem paths affected by this package action.
|
||||
pub affected_paths: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TransactionHook {
|
||||
file_path: PathBuf,
|
||||
@@ -195,11 +206,7 @@ pub fn run_transaction_hooks(rootfs: &Path, ctx: &HookExecutionContext<'_>) -> R
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let normalized_paths: Vec<String> = ctx
|
||||
.affected_paths
|
||||
.iter()
|
||||
.map(|p| normalize_match_target(p))
|
||||
.collect();
|
||||
let normalized_paths = normalize_affected_paths(ctx.affected_paths);
|
||||
|
||||
let mut executed = 0usize;
|
||||
for hook_file in hook_files {
|
||||
@@ -208,13 +215,83 @@ pub fn run_transaction_hooks(rootfs: &Path, ctx: &HookExecutionContext<'_>) -> R
|
||||
continue;
|
||||
}
|
||||
|
||||
run_hook_command(rootfs, &hook, ctx, &normalized_paths)?;
|
||||
run_hook_command(rootfs, &hook, ctx, &normalized_paths, None)?;
|
||||
executed += 1;
|
||||
}
|
||||
|
||||
Ok(executed)
|
||||
}
|
||||
|
||||
/// Load and execute hooks for a batch of transaction contexts in stable order.
|
||||
///
|
||||
/// Hooks are discovered and parsed once, then matched against each context in
|
||||
/// input order. Matching hooks run in hook-file order for each context.
|
||||
///
|
||||
/// Returns the number of hook commands executed.
|
||||
pub fn run_transaction_hooks_batch(
|
||||
rootfs: &Path,
|
||||
phase: HookPhase,
|
||||
contexts: &[HookExecutionContextOwned],
|
||||
) -> Result<usize> {
|
||||
if contexts.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let hook_dir = transaction_hooks_dir(rootfs);
|
||||
let hook_files = discover_hook_files(&hook_dir)?;
|
||||
if hook_files.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let hooks = hook_files
|
||||
.iter()
|
||||
.map(|hook_file| parse_hook_file(hook_file))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ScheduledHookRun {
|
||||
hook_idx: usize,
|
||||
ctx_idx: usize,
|
||||
}
|
||||
|
||||
let mut scheduled = Vec::new();
|
||||
for (ctx_idx, ctx_owned) in contexts.iter().enumerate() {
|
||||
let normalized_paths = normalize_affected_paths(&ctx_owned.affected_paths);
|
||||
let ctx = HookExecutionContext {
|
||||
phase,
|
||||
operation: ctx_owned.operation,
|
||||
package: &ctx_owned.package,
|
||||
affected_paths: &ctx_owned.affected_paths,
|
||||
};
|
||||
for (hook_idx, hook) in hooks.iter().enumerate() {
|
||||
if hook.matches(&ctx, &normalized_paths) {
|
||||
scheduled.push(ScheduledHookRun { hook_idx, ctx_idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let total = scheduled.len();
|
||||
for (run_idx, run) in scheduled.into_iter().enumerate() {
|
||||
let ctx_owned = &contexts[run.ctx_idx];
|
||||
let normalized_paths = normalize_affected_paths(&ctx_owned.affected_paths);
|
||||
let ctx = HookExecutionContext {
|
||||
phase,
|
||||
operation: ctx_owned.operation,
|
||||
package: &ctx_owned.package,
|
||||
affected_paths: &ctx_owned.affected_paths,
|
||||
};
|
||||
run_hook_command(
|
||||
rootfs,
|
||||
&hooks[run.hook_idx],
|
||||
&ctx,
|
||||
&normalized_paths,
|
||||
Some((run_idx + 1, total)),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn discover_hook_files(hook_dir: &Path) -> Result<Vec<PathBuf>> {
|
||||
if !hook_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
@@ -392,16 +469,28 @@ fn run_hook_command(
|
||||
hook: &TransactionHook,
|
||||
ctx: &HookExecutionContext<'_>,
|
||||
normalized_paths: &[String],
|
||||
sequence: Option<(usize, usize)>,
|
||||
) -> Result<()> {
|
||||
let hook_name = hook.display_name();
|
||||
crate::log_info!(
|
||||
"Running transaction hook '{}' ({}) for {}:{}:{}",
|
||||
hook_name,
|
||||
hook.file_path.display(),
|
||||
ctx.operation.as_str(),
|
||||
ctx.phase.as_str(),
|
||||
ctx.package
|
||||
);
|
||||
if let Some((index, total)) = sequence {
|
||||
crate::log_info!(
|
||||
"Running transaction hook ({}/{}) '{}' for {}:{}:{}",
|
||||
index,
|
||||
total,
|
||||
hook_name,
|
||||
ctx.operation.as_str(),
|
||||
ctx.phase.as_str(),
|
||||
ctx.package
|
||||
);
|
||||
} else {
|
||||
crate::log_info!(
|
||||
"Running transaction hook '{}' for {}:{}:{}",
|
||||
hook_name,
|
||||
ctx.operation.as_str(),
|
||||
ctx.phase.as_str(),
|
||||
ctx.package
|
||||
);
|
||||
}
|
||||
|
||||
let stdin_payload = if hook.needs_paths {
|
||||
let mut payload = normalized_paths.join("\n");
|
||||
@@ -468,6 +557,10 @@ fn run_command_with_optional_stdin(
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_affected_paths(paths: &[String]) -> Vec<String> {
|
||||
paths.iter().map(|p| normalize_match_target(p)).collect()
|
||||
}
|
||||
|
||||
fn normalize_match_target(raw: &str) -> String {
|
||||
raw.trim()
|
||||
.trim_start_matches("./")
|
||||
@@ -613,4 +706,42 @@ command = "touch \"$DEPOT_ROOTFS/should_not_exist\""
|
||||
assert_eq!(ran, 0);
|
||||
assert!(!tmp.path().join("should_not_exist").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_transaction_hooks_batch_executes_in_context_order() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
write_hook(
|
||||
tmp.path(),
|
||||
"batch.toml",
|
||||
r#"
|
||||
[hook]
|
||||
name = "batch"
|
||||
|
||||
[when]
|
||||
phase = "post"
|
||||
operation = ["install"]
|
||||
|
||||
[exec]
|
||||
command = "printf '%s\n' \"$DEPOT_PACKAGE\" >> \"$DEPOT_ROOTFS/batch.out\""
|
||||
"#,
|
||||
);
|
||||
|
||||
let contexts = vec![
|
||||
HookExecutionContextOwned {
|
||||
operation: HookOperation::Install,
|
||||
package: "foo".to_string(),
|
||||
affected_paths: Vec::new(),
|
||||
},
|
||||
HookExecutionContextOwned {
|
||||
operation: HookOperation::Install,
|
||||
package: "bar".to_string(),
|
||||
affected_paths: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
let ran = run_transaction_hooks_batch(tmp.path(), HookPhase::Post, &contexts).unwrap();
|
||||
assert_eq!(ran, 2);
|
||||
let out = std::fs::read_to_string(tmp.path().join("batch.out")).unwrap();
|
||||
assert_eq!(out, "foo\nbar\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -880,6 +880,19 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.ltoflags.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"ltoflags".into(),
|
||||
Value::Array(
|
||||
spec.build
|
||||
.flags
|
||||
.ltoflags
|
||||
.iter()
|
||||
.map(|s| Value::String(s.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.keep.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"keep".into(),
|
||||
@@ -893,6 +906,9 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
||||
),
|
||||
);
|
||||
}
|
||||
if spec.build.flags.use_lto != defaults.use_lto {
|
||||
flags_tbl.insert("use_lto".into(), Value::Boolean(spec.build.flags.use_lto));
|
||||
}
|
||||
if spec.build.flags.no_flags != defaults.no_flags {
|
||||
flags_tbl.insert("no_flags".into(), Value::Boolean(spec.build.flags.no_flags));
|
||||
}
|
||||
@@ -1474,8 +1490,10 @@ mod tests {
|
||||
flags.config_settings = vec!["editable_mode=compat".into()];
|
||||
flags.rustflags = vec!["-Ctarget-cpu=native".into()];
|
||||
flags.cxxflags = vec!["-O2".into(), "-fno-rtti".into()];
|
||||
flags.ltoflags = vec!["-flto=auto".into()];
|
||||
flags.target = "x86_64-unknown-linux-gnu".into();
|
||||
flags.keep = vec!["etc/locale.gen".into()];
|
||||
flags.use_lto = false;
|
||||
flags.no_flags = true;
|
||||
flags.no_strip = true;
|
||||
flags.no_delete_static = true;
|
||||
@@ -1532,9 +1550,11 @@ mod tests {
|
||||
assert!(toml.contains("config_setting = ["));
|
||||
assert!(toml.contains("rustflags = ["));
|
||||
assert!(toml.contains("cxxflags = ["));
|
||||
assert!(toml.contains("ltoflags = ["));
|
||||
assert!(toml.contains("target = \"x86_64-unknown-linux-gnu\""));
|
||||
assert!(toml.contains("keep = ["));
|
||||
assert!(toml.contains("\"etc/locale.gen\""));
|
||||
assert!(toml.contains("use_lto = false"));
|
||||
assert!(toml.contains("no_flags = true"));
|
||||
assert!(toml.contains("no_strip = true"));
|
||||
assert!(toml.contains("no_delete_static = true"));
|
||||
|
||||
@@ -300,6 +300,17 @@ impl PackageSpec {
|
||||
self.build.flags.ldflags = vec![s.to_string()];
|
||||
}
|
||||
}
|
||||
"ltoflags" | "lto_flags" | "lto-flags" => {
|
||||
if let Some(arr) = v.as_array() {
|
||||
self.build.flags.ltoflags = arr
|
||||
.iter()
|
||||
.filter_map(|x| x.as_str())
|
||||
.map(String::from)
|
||||
.collect();
|
||||
} else if let Some(s) = v.as_str() {
|
||||
self.build.flags.ltoflags = vec![s.to_string()];
|
||||
}
|
||||
}
|
||||
"keep" => {
|
||||
if let Some(arr) = v.as_array() {
|
||||
self.build.flags.keep = arr
|
||||
@@ -505,6 +516,11 @@ impl PackageSpec {
|
||||
self.build.flags.no_flags = b;
|
||||
}
|
||||
}
|
||||
"use_lto" | "use-lto" => {
|
||||
if let Some(b) = toml_value_as_boolish(v) {
|
||||
self.build.flags.use_lto = b;
|
||||
}
|
||||
}
|
||||
"no_strip" | "no-strip" => {
|
||||
if let Some(b) = v.as_bool() {
|
||||
self.build.flags.no_strip = b;
|
||||
@@ -694,6 +710,18 @@ impl PackageSpec {
|
||||
}
|
||||
}
|
||||
}
|
||||
"ltoflags" | "lto_flags" | "lto-flags" => {
|
||||
for v in values {
|
||||
if let Some(arr) = v.as_array() {
|
||||
self.build
|
||||
.flags
|
||||
.ltoflags
|
||||
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
|
||||
} else if let Some(s) = v.as_str() {
|
||||
self.build.flags.ltoflags.push(s.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
"keep" => {
|
||||
for v in values {
|
||||
if let Some(arr) = v.as_array() {
|
||||
@@ -1065,6 +1093,11 @@ impl PackageSpec {
|
||||
self.build.flags.no_flags = b;
|
||||
}
|
||||
}
|
||||
"use_lto" | "use-lto" => {
|
||||
if let Some(b) = values.last().and_then(toml_value_as_boolish) {
|
||||
self.build.flags.use_lto = b;
|
||||
}
|
||||
}
|
||||
"no_strip" | "no-strip" => {
|
||||
if let Some(b) = values.last().and_then(|v| v.as_bool()) {
|
||||
self.build.flags.no_strip = b;
|
||||
@@ -1686,6 +1719,8 @@ make_vars = ["V=1"]
|
||||
make_dirs = ["lib"]
|
||||
make_test_dirs = ["tests"]
|
||||
make_install_dirs = ["lib"]
|
||||
ltoflags = ["-flto=auto"]
|
||||
use_lto = true
|
||||
no_flags = true
|
||||
no_strip = true
|
||||
no_delete_static = true
|
||||
@@ -1719,6 +1754,16 @@ post_configure = ["echo configured"]
|
||||
"etc/locale.gen".to_string(),
|
||||
)])],
|
||||
);
|
||||
config.appends.insert(
|
||||
"build.flags.ltoflags".to_string(),
|
||||
vec![toml::Value::Array(vec![toml::Value::String(
|
||||
"-fno-fat-lto-objects".to_string(),
|
||||
)])],
|
||||
);
|
||||
config.appends.insert(
|
||||
"build.flags.use_lto".to_string(),
|
||||
vec![toml::Value::Boolean(false)],
|
||||
);
|
||||
config.appends.insert(
|
||||
"build.flags.no_strip".to_string(),
|
||||
vec![toml::Value::Boolean(false)],
|
||||
@@ -1794,6 +1839,19 @@ post_configure = ["echo configured"]
|
||||
.rustflags
|
||||
.contains(&"opt-level=3".to_string())
|
||||
);
|
||||
assert!(
|
||||
spec.build
|
||||
.flags
|
||||
.ltoflags
|
||||
.contains(&"-flto=auto".to_string())
|
||||
);
|
||||
assert!(
|
||||
spec.build
|
||||
.flags
|
||||
.ltoflags
|
||||
.contains(&"-fno-fat-lto-objects".to_string())
|
||||
);
|
||||
assert!(!spec.build.flags.use_lto);
|
||||
assert!(spec.build.flags.no_flags);
|
||||
assert!(!spec.build.flags.no_strip);
|
||||
assert!(!spec.build.flags.no_delete_static);
|
||||
@@ -1917,6 +1975,79 @@ no_flags = true
|
||||
assert!(spec.build.flags.no_flags);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ltoflags_and_use_lto_from_spec() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let path = tmp.path().join("pkg.toml");
|
||||
|
||||
std::fs::write(
|
||||
&path,
|
||||
r#"
|
||||
[package]
|
||||
name = "foo"
|
||||
version = "1.0"
|
||||
description = "d"
|
||||
homepage = "h"
|
||||
license = "MIT"
|
||||
|
||||
[source]
|
||||
url = "https://example.com/foo.tar.gz"
|
||||
sha256 = "skip"
|
||||
extract_dir = "foo"
|
||||
|
||||
[build]
|
||||
type = "custom"
|
||||
|
||||
[build.flags]
|
||||
ltoflags = ["-flto=auto", "-fuse-linker-plugin"]
|
||||
use_lto = false
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let spec = PackageSpec::from_file(&path).unwrap();
|
||||
assert_eq!(
|
||||
spec.build.flags.ltoflags,
|
||||
vec!["-flto=auto".to_string(), "-fuse-linker-plugin".to_string()]
|
||||
);
|
||||
assert!(!spec.build.flags.use_lto);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ltoflags_and_use_lto_aliases_from_spec() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let path = tmp.path().join("pkg.toml");
|
||||
|
||||
std::fs::write(
|
||||
&path,
|
||||
r#"
|
||||
[package]
|
||||
name = "foo"
|
||||
version = "1.0"
|
||||
description = "d"
|
||||
homepage = "h"
|
||||
license = "MIT"
|
||||
|
||||
[source]
|
||||
url = "https://example.com/foo.tar.gz"
|
||||
sha256 = "skip"
|
||||
extract_dir = "foo"
|
||||
|
||||
[build]
|
||||
type = "custom"
|
||||
|
||||
[build.flags]
|
||||
LTOFLAGS = "-flto=auto"
|
||||
"use-lto" = false
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let spec = PackageSpec::from_file(&path).unwrap();
|
||||
assert_eq!(spec.build.flags.ltoflags, vec!["-flto=auto".to_string()]);
|
||||
assert!(!spec.build.flags.use_lto);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_strip_no_delete_static_and_no_compress_man_from_spec() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -2800,9 +2931,28 @@ pub struct BuildFlags {
|
||||
pub cxxflags_lib32: Vec<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
||||
pub ldflags: Vec<String>,
|
||||
/// Link-time optimization flags exported to `LTOFLAGS`.
|
||||
///
|
||||
/// When `use_lto` is true (default), these flags are also appended to
|
||||
/// `CFLAGS`, `CXXFLAGS`, and `LDFLAGS`.
|
||||
#[serde(
|
||||
default,
|
||||
alias = "lto-flags",
|
||||
alias = "lto_flags",
|
||||
alias = "LTOFLAGS",
|
||||
deserialize_with = "deserialize_string_or_array"
|
||||
)]
|
||||
pub ltoflags: Vec<String>,
|
||||
/// Keep existing files and install package-provided replacement as `<path>.depotnew`.
|
||||
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
||||
pub keep: Vec<String>,
|
||||
/// Disable automatic LTOFLAGS injection into CFLAGS/CXXFLAGS/LDFLAGS.
|
||||
#[serde(
|
||||
default = "default_use_lto",
|
||||
alias = "use-lto",
|
||||
deserialize_with = "deserialize_boolish"
|
||||
)]
|
||||
pub use_lto: bool,
|
||||
/// Disable exporting CFLAGS/CXXFLAGS/LDFLAGS for this package build.
|
||||
#[serde(default, alias = "no-flags")]
|
||||
pub no_flags: bool,
|
||||
@@ -3071,7 +3221,9 @@ impl Default for BuildFlags {
|
||||
cxxflags: Vec::new(),
|
||||
cxxflags_lib32: Vec::new(),
|
||||
ldflags: Vec::new(),
|
||||
ltoflags: Vec::new(),
|
||||
keep: Vec::new(),
|
||||
use_lto: default_use_lto(),
|
||||
no_flags: false,
|
||||
no_strip: false,
|
||||
no_delete_static: false,
|
||||
@@ -3256,6 +3408,10 @@ fn default_cc() -> String {
|
||||
"gcc".to_string()
|
||||
}
|
||||
|
||||
fn default_use_lto() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_ar() -> String {
|
||||
"ar".to_string()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user