feat: update user configuration example and add development package support

- Enhanced the user.depot.toml.example file with additional comments and options for user-local preferences.
- Bumped version from 0.35.0 to 0.35.1 in meson.build.
- Introduced a new option for DEPOT_DEVELOPMENT_PACKAGE in meson_options.txt.
- Added functions to handle the requested development package in build_options.rs.
- Updated various builder files (autotools.rs, cmake.rs, custom.rs, meson.rs, perl.rs) to support passing build flags.
- Implemented checks for the required development package during source builds in commands.rs and commands/build_cmd.rs.
- Added tests to ensure the correct behavior of development package requirements and source build warnings.
- Modified manual source handling in source/mod.rs to validate local manual sources before proceeding with builds.
- Updated cargo build and test scripts to accept a new development package argument.
- Added VSCode settings to disable automatic configuration on open.
This commit is contained in:
2026-04-04 17:02:42 -05:00
parent 135514acb3
commit 34d52d11a9
24 changed files with 866 additions and 164 deletions
+18 -1
View File
@@ -2,6 +2,7 @@ use crate::package::BuildType;
use anyhow::Result;
const BUILD_DEPOT_STATIC_OPTION: &str = "BUILD_DEPOT_STATIC";
const DEPOT_DEVELOPMENT_PACKAGE_OPTION: &str = "DEPOT_DEVELOPMENT_PACKAGE";
fn parse_boolish_option(value: &str) -> Option<bool> {
match value.trim().to_ascii_lowercase().as_str() {
@@ -63,9 +64,20 @@ pub(crate) fn requested_build_tool_package(build_type: BuildType) -> Option<Stri
}
}
pub(crate) fn development_package_option() -> &'static str {
DEPOT_DEVELOPMENT_PACKAGE_OPTION
}
pub(crate) fn requested_development_package() -> Option<String> {
normalize_string_option(option_env!("DEPOT_DEVELOPMENT_PACKAGE"))
}
#[cfg(test)]
mod tests {
use super::{build_tool_package_option, normalize_string_option, parse_boolish_option};
use super::{
build_tool_package_option, development_package_option, normalize_string_option,
parse_boolish_option,
};
use crate::package::BuildType;
#[test]
@@ -99,4 +111,9 @@ mod tests {
);
assert_eq!(build_tool_package_option(BuildType::Bin), None);
}
#[test]
fn development_package_option_matches_expected_name() {
assert_eq!(development_package_option(), "DEPOT_DEVELOPMENT_PACKAGE");
}
}
+6 -2
View File
@@ -134,7 +134,9 @@ pub fn build(
let expanded = expand_configure_arg(spec, arg, &env_vars);
configure_cmd.arg(expanded);
}
for arg in crate::builder::static_build_args_for(crate::package::BuildType::Autotools)? {
for arg in
crate::builder::static_build_args_for(crate::package::BuildType::Autotools, flags)?
{
add_auto_configure_arg_if_supported(&mut configure_cmd, help_text.as_deref(), &arg);
}
@@ -444,7 +446,9 @@ pub(crate) fn ensure_host_build(
let expanded = expand_configure_arg(&host_spec, arg, &env_vars);
configure_cmd.arg(expanded);
}
for arg in crate::builder::static_build_args_for(crate::package::BuildType::Autotools)? {
for arg in
crate::builder::static_build_args_for(crate::package::BuildType::Autotools, flags)?
{
add_auto_configure_arg_if_supported(&mut configure_cmd, help_text.as_deref(), &arg);
}
+2 -2
View File
@@ -97,7 +97,7 @@ pub fn build(
let expanded = expand_with_envs(flag, &env_vars);
cmake_cmd.arg(&expanded);
}
for arg in crate::builder::static_build_args_for(crate::package::BuildType::CMake)? {
for arg in crate::builder::static_build_args_for(crate::package::BuildType::CMake, flags)? {
cmake_cmd.arg(arg);
}
@@ -317,7 +317,7 @@ pub(crate) fn ensure_host_build(
let expanded = expand_with_envs(flag, &env_vars);
cmake_cmd.arg(&expanded);
}
for arg in crate::builder::static_build_args_for(crate::package::BuildType::CMake)? {
for arg in crate::builder::static_build_args_for(crate::package::BuildType::CMake, flags)? {
cmake_cmd.arg(arg);
}
+187 -32
View File
@@ -8,6 +8,7 @@ use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
use std::process::Command;
use tempfile::NamedTempFile;
pub fn build(
spec: &PackageSpec,
@@ -98,15 +99,6 @@ pub fn build(
}
if !state.is_done(BuildStep::PostInstallDone) {
crate::log_info!(
"Running custom build script{}...",
if fakeroot::is_root() {
""
} else {
" (with fakeroot)"
}
);
crate::builder::set_env_var(
&mut env_vars,
"DESTDIR",
@@ -139,13 +131,31 @@ pub fn build(
std::env::current_dir()?.join(&build_script)
};
// Use POSIX `sh` (more likely to be available in minimal/chroot environments)
let mut cmd = if custom_function_mode_enabled(&abs_build_script)? {
let function_mode = custom_function_mode_enabled(&abs_build_script)?;
if function_mode {
crate::log_info!(
"Running custom build script (function mode; fakeroot only during install)..."
);
crate::log_info!(
"Using custom build.sh function mode (per-output install functions enabled)"
);
build_function_mode_command(spec, &install_destdir, &abs_build_script)?
run_function_mode_build_script(
spec,
&build_dir,
&install_destdir,
&abs_build_script,
&env_vars,
)?;
} else {
crate::log_info!(
"Running custom build script{}...",
if fakeroot::is_root() {
""
} else {
" (with fakeroot)"
}
);
// Use POSIX `sh` (doing something wrong if your system doesn't have it...)
let mut cmd = fakeroot::wrap_install_command("sh", &install_destdir);
let wrapper = crate::shell_helpers::wrap_shell_command(". \"$1\"");
// Run custom scripts through `sh -c` so helper commands like `haul`
@@ -155,23 +165,22 @@ pub fn build(
.arg(wrapper)
.arg("sh")
.arg(&abs_build_script);
cmd
};
cmd.current_dir(&build_dir);
cmd.current_dir(&build_dir);
crate::builder::prepare_tool_command(&mut cmd, &env_vars);
crate::builder::prepare_tool_command(&mut cmd, &env_vars);
// Run the command and include the OS error on spawn failures for clearer diagnostics
let status = crate::interrupts::command_status(&mut cmd).map_err(|e| {
anyhow::anyhow!(
"Failed to run build script {}: {}",
build_script.display(),
e
)
})?;
// Run the command and include the OS error on spawn failures for clearer diagnostics
let status = crate::interrupts::command_status(&mut cmd).map_err(|e| {
anyhow::anyhow!(
"Failed to run build script {}: {}",
build_script.display(),
e
)
})?;
if !status.success() {
anyhow::bail!("Custom build script failed with status: {}", status);
if !status.success() {
anyhow::bail!("Custom build script failed with status: {}", status);
}
}
if flags.lib32_variant {
crate::builder::stage_lib32_install_tree(&install_destdir, destdir)?;
@@ -213,11 +222,7 @@ fn custom_function_mode_enabled(build_script: &Path) -> Result<bool> {
|| contents.contains("install_"))
}
fn build_function_mode_command(
spec: &PackageSpec,
destdir: &Path,
build_script: &Path,
) -> Result<Command> {
fn function_mode_shell_prelude() -> String {
let mut wrapper = crate::shell_helpers::wrap_shell_command("");
wrapper.push_str("\nset -eu\n");
wrapper.push_str("depot_has_function() {\n");
@@ -226,11 +231,39 @@ fn build_function_mode_command(
wrapper.push_str(" *) return 1 ;;\n");
wrapper.push_str(" esac\n");
wrapper.push_str("}\n");
wrapper
}
fn build_function_mode_build_command(build_script: &Path, state_file: &Path) -> Command {
let mut wrapper = function_mode_shell_prelude();
wrapper.push_str("depot_build_ran=0\n");
wrapper.push_str(". \"$1\"\n");
wrapper.push_str("if depot_has_function depot_build; then depot_build; depot_build_ran=1;\n");
wrapper.push_str("elif depot_has_function build; then build; depot_build_ran=1;\n");
wrapper.push_str("fi\n");
wrapper.push_str("printf '%s\\n' \"$depot_build_ran\" > \"$2\"\n");
let mut cmd = Command::new("sh");
cmd.arg("-c")
.arg(wrapper)
.arg("sh")
.arg(build_script)
.arg(state_file);
cmd
}
fn build_function_mode_install_command(
spec: &PackageSpec,
destdir: &Path,
build_script: &Path,
build_ran: bool,
) -> Command {
let mut wrapper = function_mode_shell_prelude();
wrapper.push_str(&format!(
"depot_build_ran={}\n",
if build_ran { 1 } else { 0 }
));
wrapper.push_str(". \"$1\"\n");
let primary = &spec.package.name;
for out in spec.outputs() {
@@ -272,7 +305,65 @@ fn build_function_mode_command(
let mut cmd = fakeroot::wrap_install_command("sh", destdir);
cmd.arg("-c").arg(wrapper).arg("sh").arg(build_script);
Ok(cmd)
cmd
}
fn run_function_mode_build_script(
spec: &PackageSpec,
build_dir: &Path,
install_destdir: &Path,
build_script: &Path,
env_vars: &crate::builder::EnvVars,
) -> Result<()> {
let state_file = NamedTempFile::new_in(build_dir).with_context(|| {
format!(
"Failed to create function mode state file in {}",
build_dir.display()
)
})?;
let state_path = state_file.path().to_path_buf();
let mut build_cmd = build_function_mode_build_command(build_script, &state_path);
build_cmd.current_dir(build_dir);
let mut build_env = env_vars.clone();
crate::builder::set_env_var(
&mut build_env,
"DESTDIR",
install_destdir.to_string_lossy().into_owned(),
);
crate::builder::prepare_tool_command(&mut build_cmd, &build_env);
let build_status = crate::interrupts::command_status(&mut build_cmd).map_err(|e| {
anyhow::anyhow!(
"Failed to run build script {}: {}",
build_script.display(),
e
)
})?;
if !build_status.success() {
anyhow::bail!("Custom build script failed with status: {}", build_status);
}
let build_ran = fs::read_to_string(&state_path)
.ok()
.map(|value| value.trim() == "1")
.unwrap_or(false);
let mut install_cmd =
build_function_mode_install_command(spec, install_destdir, build_script, build_ran);
install_cmd.current_dir(build_dir);
crate::builder::prepare_tool_command(&mut install_cmd, env_vars);
let install_status = crate::interrupts::command_status(&mut install_cmd).map_err(|e| {
anyhow::anyhow!(
"Failed to run build script {}: {}",
build_script.display(),
e
)
})?;
if !install_status.success() {
anyhow::bail!("Custom build script failed with status: {}", install_status);
}
Ok(())
}
fn shell_fn_suffix(pkg_name: &str) -> String {
@@ -430,6 +521,70 @@ depot_install_dev_pkg() {
Ok(())
}
#[test]
fn test_build_function_mode_only_uses_fakeroot_for_install() -> Result<()> {
let tmp_src = tempdir()?;
let tmp_dest = tempdir()?;
let build_sh = tmp_src.path().join("build.sh");
std::fs::write(
&build_sh,
r#"#!/bin/sh
depot_build() {
if [ "${FAKEROOT_ACTIVE:-0}" = 1 ]; then
echo yes > build-fakeroot.txt
else
echo no > build-fakeroot.txt
fi
}
depot_install() {
mkdir -p "$DESTDIR/usr/share"
echo installed > "$DESTDIR/usr/share/install-fakeroot.txt"
}
"#,
)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&build_sh)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&build_sh, perms)?;
}
let spec = mk_spec("custom-function-fakeroot-split", "1.0");
build(&spec, tmp_src.path(), tmp_dest.path(), None, true, None)?;
assert_eq!(
std::fs::read_to_string(tmp_src.path().join("build-fakeroot.txt"))?,
"no\n"
);
assert_eq!(
std::fs::read_to_string(tmp_dest.path().join("usr/share/install-fakeroot.txt"))?,
"installed\n"
);
Ok(())
}
#[test]
fn test_build_function_mode_commands_split_fakeroot_boundary() {
let build_script = Path::new("/tmp/build.sh");
let state_file = Path::new("/tmp/build-state");
let install_destdir = Path::new("/tmp/destdir");
let build_cmd = build_function_mode_build_command(build_script, state_file);
assert_eq!(build_cmd.get_program(), std::ffi::OsStr::new("sh"));
let spec = mk_spec("custom-function-fakeroot-split", "1.0");
let install_cmd =
build_function_mode_install_command(&spec, install_destdir, build_script, true);
let expected = if crate::fakeroot::is_root() {
std::ffi::OsStr::new("sh")
} else {
std::ffi::OsStr::new("fakeroot")
};
assert_eq!(install_cmd.get_program(), expected);
}
#[test]
fn test_build_function_mode_errors_when_output_handler_missing() -> Result<()> {
let tmp_src = tempdir()?;
+2 -2
View File
@@ -65,7 +65,7 @@ pub fn build(
for arg in meson_setup_args(flags, cross_file.as_deref(), &env_vars) {
meson_cmd.arg(arg);
}
for arg in crate::builder::static_build_args_for(crate::package::BuildType::Meson)? {
for arg in crate::builder::static_build_args_for(crate::package::BuildType::Meson, flags)? {
meson_cmd.arg(arg);
}
@@ -232,7 +232,7 @@ pub(crate) fn ensure_host_build(
for arg in meson_setup_args(flags, None, &env_vars) {
meson_cmd.arg(arg);
}
for arg in crate::builder::static_build_args_for(crate::package::BuildType::Meson)? {
for arg in crate::builder::static_build_args_for(crate::package::BuildType::Meson, flags)? {
meson_cmd.arg(arg);
}
+53 -7
View File
@@ -12,7 +12,7 @@ mod rust;
pub mod state;
use crate::cross::CrossConfig;
use crate::package::{BuildType, PackageSpec};
use crate::package::{BuildFlags, BuildType, PackageSpec};
use anyhow::{Context, Result};
use std::ffi::OsString;
use std::fs;
@@ -188,12 +188,20 @@ pub(crate) fn requested_static_build() -> Result<Option<bool>> {
crate::build_options::requested_static_build()
}
pub(crate) fn static_build_args_for(build_type: BuildType) -> Result<Vec<String>> {
let Some(enabled) = requested_static_build()? else {
return Ok(Vec::new());
fn static_build_args_for_request(
build_type: BuildType,
requested_static: Option<bool>,
no_delete_static: bool,
) -> Vec<String> {
let Some(enabled) = requested_static else {
return Vec::new();
};
let args = match build_type {
if !enabled && no_delete_static {
return Vec::new();
}
match build_type {
BuildType::Autotools => vec![if enabled {
"--enable-static".to_string()
} else {
@@ -212,9 +220,18 @@ pub(crate) fn static_build_args_for(build_type: BuildType) -> Result<Vec<String>
if enabled { "static" } else { "dynamic" }
)],
_ => Vec::new(),
};
}
}
Ok(args)
pub(crate) fn static_build_args_for(
build_type: BuildType,
flags: &BuildFlags,
) -> Result<Vec<String>> {
Ok(static_build_args_for_request(
build_type,
requested_static_build()?,
flags.no_delete_static,
))
}
pub(crate) fn build_tool_package_option(build_type: BuildType) -> Option<&'static str> {
@@ -225,6 +242,14 @@ pub(crate) fn requested_build_tool_package(build_type: BuildType) -> Option<Stri
crate::build_options::requested_build_tool_package(build_type)
}
pub(crate) fn development_package_option() -> &'static str {
crate::build_options::development_package_option()
}
pub(crate) fn requested_development_package() -> Option<String> {
crate::build_options::requested_development_package()
}
fn configured_install_dir(value: &str, default: &str) -> String {
let trimmed = value.trim();
if trimmed.is_empty() {
@@ -943,6 +968,27 @@ mod tests {
assert_eq!(build_tool_package_option(BuildType::Bin), None);
}
#[test]
fn test_static_build_args_skip_disable_static_when_no_delete_static_enabled() {
let args = static_build_args_for_request(BuildType::Autotools, Some(false), true);
assert!(args.is_empty());
let args = static_build_args_for_request(BuildType::CMake, Some(false), true);
assert!(args.is_empty());
}
#[test]
fn test_static_build_args_keep_other_requested_modes() {
assert_eq!(
static_build_args_for_request(BuildType::Autotools, Some(false), false),
vec!["--disable-static".to_string()]
);
assert_eq!(
static_build_args_for_request(BuildType::Meson, Some(true), true),
vec!["-Ddefault_library=static".to_string()]
);
}
#[test]
fn test_standard_build_env_exports_native_linker_and_cpp() {
let mut spec = mk_spec(Vec::new(), Vec::new());
+1 -1
View File
@@ -59,7 +59,7 @@ pub fn build(
for arg in &flags.configure {
configure_cmd.arg(spec.expand_vars(arg));
}
for arg in crate::builder::static_build_args_for(crate::package::BuildType::Perl)? {
for arg in crate::builder::static_build_args_for(crate::package::BuildType::Perl, flags)? {
configure_cmd.arg(arg);
}
crate::builder::prepare_tool_command(&mut configure_cmd, &env_vars);
+95 -18
View File
@@ -20,9 +20,9 @@ use walkdir::WalkDir;
use build_cmd::support::{
automatic_tests_disabled_for_outputs, build_lib32_companion_package, clean_build_workspace,
effective_lib32_only, make_lib32_package_spec, maybe_disable_tests_for_missing_deps,
maybe_prompt_to_skip_tests_for_missing_requested_deps, merge_missing_dependencies,
requested_outputs, should_install_test_deps,
effective_lib32_only, ensure_requested_development_package_installed, make_lib32_package_spec,
maybe_disable_tests_for_missing_deps, maybe_prompt_to_skip_tests_for_missing_requested_deps,
merge_missing_dependencies, requested_outputs, should_install_test_deps,
};
use install_cmd::archive::{
extract_package_archive_to_staging, load_package_archive_into_staging,
@@ -1420,15 +1420,88 @@ fn actionable_plan_packages(plan: &planner::ExecutionPlan) -> Vec<String> {
.collect()
}
fn warn_source_build_installs(count: usize) {
if count > 0 {
ui::warn(format!(
"{} package(s) will be built from source before installation.",
count
));
fn source_build_reason(reason: &str) -> String {
if let Some(dep) = reason.strip_prefix("dependency ") {
format!("requested dependency '{dep}'")
} else if let Some((requester, _)) = reason.split_once(" needs ") {
format!("needed by '{requester}'")
} else if reason == "requested spec" {
"requested spec".to_string()
} else if reason == "requested package" {
"requested package".to_string()
} else {
reason.to_string()
}
}
fn source_build_warning_messages(plan: &planner::ExecutionPlan) -> Vec<String> {
let mut lines = Vec::new();
for step in plan.actionable_steps() {
if !matches!(step.action, planner::PlanAction::BuildAndInstall) {
continue;
}
let mut reasons = Vec::new();
for reason in &step.requested_by {
let label = source_build_reason(reason);
if !reasons.contains(&label) {
reasons.push(label);
}
}
if reasons.is_empty() {
lines.push(step.package.clone());
} else {
lines.push(format!("{} ({})", step.package, reasons.join(", ")));
}
}
lines
}
fn warn_source_build_plan(plan: &planner::ExecutionPlan) {
let lines = source_build_warning_messages(plan);
if lines.is_empty() {
return;
}
ui::warn(format!(
"{} package(s) will be built from source before installation.",
lines.len()
));
for line in lines {
ui::warn(format!(" {line}"));
}
}
fn validate_source_build_prereqs_for_plan(
plan: &planner::ExecutionPlan,
rootfs: &Path,
config: &config::Config,
) -> Result<()> {
let db_path = config.installed_db_path(rootfs);
let mut checked_development_package = false;
for step in plan.actionable_steps() {
let planner::PlanOrigin::Source { path, .. } = &step.origin else {
continue;
};
if !matches!(step.action, planner::PlanAction::BuildAndInstall) {
continue;
}
let mut spec = package::PackageSpec::from_file(path)
.with_context(|| format!("Failed to parse spec {}", path.display()))?;
spec.apply_config(config);
source::preflight_local_manual_sources(&spec)?;
if !checked_development_package && !spec.is_metapackage() {
ensure_requested_development_package_installed(&db_path)?;
checked_development_package = true;
}
}
Ok(())
}
#[derive(Clone, Copy)]
struct InstallPlanExecutionOptions<'a> {
no_flags: bool,
@@ -1532,14 +1605,14 @@ fn execute_install_plan_with_child_commands(
record: db::repo::BinaryRepoPackageRecord,
}
let summary = plan.summary();
let actionable_steps: Vec<_> = plan.actionable_steps().collect();
if actionable_steps.is_empty() {
ui::info("Nothing to do.");
return Ok(());
}
warn_source_build_installs(summary.source_build_installs);
validate_source_build_prereqs_for_plan(plan, rootfs, config)?;
warn_source_build_plan(plan);
let planned_packages = actionable_plan_packages(plan);
if options.confirm_installation
&& !ui::prompt_package_action("installation", &planned_packages, true)?
@@ -2083,6 +2156,14 @@ fn run_direct_install_request(
}
let requested_outputs = requested_outputs(&pkg_spec, lib32_only);
let db_path = config.installed_db_path(options.rootfs);
if staging_dir.is_none() {
source::preflight_local_manual_sources(&pkg_spec)?;
if !pkg_spec.is_metapackage() {
ensure_requested_development_package_installed(&db_path)?;
}
}
let mut conflict_subjects = install_conflict_subjects_for_spec(
&pkg_spec,
@@ -2122,11 +2203,6 @@ fn run_direct_install_request(
config.db_dir.display()
)
})?;
let db_path = config.installed_db_path(options.rootfs);
if staging_dir.is_none() {
source::preflight_manual_sources(&pkg_spec, &config.cache_dir)?;
}
if staging_dir.is_none() {
if options.no_deps
@@ -2190,7 +2266,7 @@ fn run_direct_install_request(
},
)?;
let dep_plan_packages = actionable_plan_packages(&dep_plan);
warn_source_build_installs(dep_plan.summary().source_build_installs);
warn_source_build_plan(&dep_plan);
let dep_prompt_packages = if dep_plan_packages.is_empty() {
missing_required.clone()
} else {
@@ -2293,7 +2369,7 @@ fn run_direct_install_request(
},
)?;
let dep_plan_packages = actionable_plan_packages(&dep_plan);
warn_source_build_installs(dep_plan.summary().source_build_installs);
warn_source_build_plan(&dep_plan);
let dep_prompt_packages = if dep_plan_packages.is_empty() {
missing_test.clone()
} else {
@@ -2362,6 +2438,7 @@ fn run_direct_install_request(
dir.path().to_path_buf()
} else {
// 1-2. Fetch + extract sources (supports archives and git URL#rev)
source::preflight_manual_sources(&pkg_spec, &config.cache_dir)?;
let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?;
built_src_dir = Some(src_dir.clone());
let host_build_dir = builder::ensure_host_build(
+11 -4
View File
@@ -5,9 +5,10 @@ pub(crate) mod support;
use self::support::{
RequestedBuildToolPackageInstall, automatic_tests_disabled_for_outputs,
build_lib32_companion_package, clean_build_workspace, effective_lib32_only,
ensure_requested_build_tool_package_installed, maybe_disable_tests_for_missing_deps,
maybe_prompt_to_skip_tests_for_missing_requested_deps, merge_missing_dependencies,
requested_outputs, should_install_test_deps, warn_if_running_as_root_for_build,
ensure_requested_build_tool_package_installed, ensure_requested_development_package_installed,
maybe_disable_tests_for_missing_deps, maybe_prompt_to_skip_tests_for_missing_requested_deps,
merge_missing_dependencies, requested_outputs, should_install_test_deps,
warn_if_running_as_root_for_build,
};
pub(super) fn run_build(args: BuildArgs, cli_test_deps: bool) -> Result<()> {
@@ -47,6 +48,12 @@ pub(super) fn run_build(args: BuildArgs, cli_test_deps: bool) -> Result<()> {
pkg_spec.apply_config(&config);
let lib32_only = effective_lib32_only(&pkg_spec, cli_lib32_only);
let requested_outputs = requested_outputs(&pkg_spec, lib32_only);
let db_path = config.installed_db_path(&rootfs);
source::preflight_local_manual_sources(&pkg_spec)?;
if !pkg_spec.is_metapackage() {
ensure_requested_development_package_installed(&db_path)?;
}
let build_targets = vec![format!(
"{} v{}-{}",
@@ -62,7 +69,6 @@ pub(super) fn run_build(args: BuildArgs, cli_test_deps: bool) -> Result<()> {
config.db_dir.display()
)
})?;
let db_path = config.installed_db_path(&rootfs);
if no_deps && should_install_test_deps(&pkg_spec, install_test_deps, requested_outputs) {
let missing_test =
@@ -297,6 +303,7 @@ pub(super) fn run_build(args: BuildArgs, cli_test_deps: bool) -> Result<()> {
watcher.check()?;
}
source::preflight_manual_sources(&pkg_spec, &config.cache_dir)?;
let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?;
if let Some(watcher) = interrupt_watcher.as_ref() {
watcher.check()?;
+98
View File
@@ -247,6 +247,30 @@ pub(crate) fn ensure_requested_build_tool_package_installed(
)
}
fn ensure_requested_development_package_installed_for(
package_name: Option<&str>,
db_path: &Path,
) -> Result<()> {
let Some(package_name) = package_name.filter(|name| !name.trim().is_empty()) else {
return Ok(());
};
if deps::is_dep_satisfied_in_db(package_name, db_path)? {
return Ok(());
}
anyhow::bail!(
"Missing required development package for source builds ({}): {}. Install it first before building packages from source.",
builder::development_package_option(),
package_name
);
}
pub(crate) fn ensure_requested_development_package_installed(db_path: &Path) -> Result<()> {
let package_name = builder::requested_development_package();
ensure_requested_development_package_installed_for(package_name.as_deref(), db_path)
}
pub(crate) fn build_lib32_companion_package(
pkg_spec: &package::PackageSpec,
src_dir: &Path,
@@ -323,3 +347,77 @@ pub(crate) fn build_lib32_companion_package(
Ok(Some((lib32_pkg_spec, lib32_destdir)))
}
#[cfg(test)]
mod tests {
use super::ensure_requested_development_package_installed_for;
use crate::config;
use crate::db;
use crate::package::{
Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
};
use anyhow::Result;
use std::fs;
use std::path::PathBuf;
fn test_spec(name: &str) -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: name.into(),
real_name: None,
version: "1.0.0".into(),
revision: 1,
description: "d".into(),
homepage: "https://example.test".into(),
abi_breaking: false,
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.test/src.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "src".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Custom,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
}
}
#[test]
fn requested_development_package_requirement_fails_when_missing() {
let temp = tempfile::tempdir().unwrap();
let db_path = temp.path().join("installed.sqlite");
let err =
ensure_requested_development_package_installed_for(Some("development-base"), &db_path)
.expect_err("missing development package should fail");
assert!(err.to_string().contains("development-base"));
}
#[test]
fn requested_development_package_requirement_passes_when_installed() -> Result<()> {
let rootfs = tempfile::tempdir()?;
let config = config::Config::for_rootfs(rootfs.path());
fs::create_dir_all(&config.db_dir)?;
let db_path = config.installed_db_path(rootfs.path());
let spec = test_spec("development-base");
let dest = rootfs.path().join("dest").join("development-base");
fs::create_dir_all(dest.join("usr/bin"))?;
fs::write(dest.join("usr/bin/dev-base"), "bin")?;
db::register_package(&db_path, &spec, &dest)?;
ensure_requested_development_package_installed_for(Some("development-base"), &db_path)?;
Ok(())
}
}
+153
View File
@@ -2214,6 +2214,159 @@ file = "missing.patch"
Ok(())
}
#[test]
fn build_command_checks_manual_sources_before_dependency_resolution() -> Result<()> {
let _guard = ASSUME_YES_TEST_LOCK.lock().expect("assume-yes test lock");
let temp = tempfile::tempdir().context("Failed to create temp dir")?;
let rootfs = temp.path().join("rootfs");
let spec_dir = temp.path().join("packages").join("demo");
fs::create_dir_all(&rootfs)?;
fs::create_dir_all(&spec_dir)?;
let spec_path = spec_dir.join("demo.toml");
fs::write(
&spec_path,
r#"[package]
name = "demo"
version = "1.0.0"
revision = 1
description = "demo"
homepage = "https://example.test/demo"
license = "MIT"
[[source]]
url = "https://example.test/demo-1.0.0.tar.gz"
sha256 = "skip"
extract_dir = "demo-1.0.0"
[build]
type = "custom"
[dependencies]
build = ["definitely-missing-dep"]
runtime = []
optional = []
[[manual_sources]]
file = "missing.patch"
"#,
)?;
let result = run(Cli {
command: Commands::Build(BuildArgs {
rootfs_args: rootfs_args(rootfs),
prompt_args: prompt_args(true),
build_exec_args: build_exec_args(),
lib32_args: lib32_args(),
spec_pos: Some(spec_path),
spec: None,
install: false,
install_deps: true,
cleanup_deps: false,
}),
});
let err = result.expect_err("missing manual source should fail before dependency install");
assert!(
err.to_string()
.contains("Manual source not found: missing.patch")
);
assert!(
!err.to_string()
.contains("Failed to resolve required build tool package")
);
Ok(())
}
#[test]
fn source_build_warning_messages_include_dependency_context() {
let plan = planner::ExecutionPlan {
steps: vec![
planner::PlannedStep {
package: "dep-src".into(),
action: planner::PlanAction::BuildAndInstall,
origin: planner::PlanOrigin::Source {
path: PathBuf::from("/tmp/dep-src.toml"),
local_sibling: false,
},
requested_by: vec!["dependency dep-src".into(), "app needs dep-src".into()],
},
planner::PlannedStep {
package: "dep-bin".into(),
action: planner::PlanAction::InstallBinary,
origin: planner::PlanOrigin::Binary {
repo_name: "core".into(),
record: Box::new(test_binary_repo_record(
"dep-bin",
"dep-bin-1.0-1-x86_64.tar.zst",
)),
},
requested_by: vec!["app needs dep-bin".into()],
},
],
};
assert_eq!(
source_build_warning_messages(&plan),
vec!["dep-src (requested dependency 'dep-src', needed by 'app')".to_string()]
);
}
#[test]
fn planned_source_build_prereqs_check_manual_sources_before_confirmation() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let spec_dir = tempfile::tempdir().context("Failed to create temp spec dir")?;
let spec_path = spec_dir.path().join("demo.toml");
fs::write(
&spec_path,
r#"[package]
name = "demo"
version = "1.0.0"
revision = 1
description = "demo"
homepage = "https://example.test/demo"
license = "MIT"
[[source]]
url = "https://example.test/demo-1.0.0.tar.gz"
sha256 = "skip"
extract_dir = "demo-1.0.0"
[build]
type = "custom"
[dependencies]
build = []
runtime = []
optional = []
[[manual_sources]]
file = "missing.patch"
"#,
)?;
let config = config::Config::for_rootfs(rootfs.path());
let plan = planner::ExecutionPlan {
steps: vec![planner::PlannedStep {
package: "demo".into(),
action: planner::PlanAction::BuildAndInstall,
origin: planner::PlanOrigin::Source {
path: spec_path,
local_sibling: true,
},
requested_by: vec!["requested spec".into()],
}],
};
let err = validate_source_build_prereqs_for_plan(&plan, rootfs.path(), &config)
.expect_err("missing local manual source should fail before confirmation");
assert!(
err.to_string()
.contains("Manual source not found: missing.patch")
);
Ok(())
}
#[test]
fn suppress_nested_install_output_for_planned_context() {
let mut env = TestEnv::new();
+31 -17
View File
@@ -62,11 +62,42 @@ fn manual_url_entries(manual: &crate::package::ManualSource) -> Vec<String> {
/// Remote entries are fetched into the manual-source cache so build-time source
/// preparation can reuse the verified result later. Git manual sources prime
/// their mirror cache and validate revision reachability.
pub fn preflight_local_manual_sources(spec: &PackageSpec) -> Result<()> {
if spec.manual_sources.is_empty() {
return Ok(());
}
for manual in &spec.manual_sources {
let local_entries = manual_local_entries(manual);
for raw_file in local_entries {
let file = expand_manual_source_value(spec, &raw_file);
let src_path = spec.spec_dir.join(&file);
if !src_path.exists() {
bail!(
"Manual source not found: {} (expected at {})",
file,
src_path.display()
);
}
if let Some(expected_hash) = manual.sha256.as_ref().filter(|h| *h != "skip")
&& !verify_file_hash(&src_path, expected_hash)?
{
bail!("Checksum mismatch for {}: expected {}", file, expected_hash);
}
}
}
Ok(())
}
pub fn preflight_manual_sources(spec: &PackageSpec, cache_dir: &Path) -> Result<()> {
if spec.manual_sources.is_empty() {
return Ok(());
}
preflight_local_manual_sources(spec)?;
let manual_entry_count: usize = spec
.manual_sources
.iter()
@@ -79,23 +110,6 @@ pub fn preflight_manual_sources(spec: &PackageSpec, cache_dir: &Path) -> Result<
let url_entries = manual_url_entries(manual);
if !local_entries.is_empty() {
for raw_file in local_entries {
let file = expand_manual_source_value(spec, &raw_file);
let src_path = spec.spec_dir.join(&file);
if !src_path.exists() {
bail!(
"Manual source not found: {} (expected at {})",
file,
src_path.display()
);
}
if let Some(expected_hash) = manual.sha256.as_ref().filter(|h| *h != "skip")
&& !verify_file_hash(&src_path, expected_hash)?
{
bail!("Checksum mismatch for {}: expected {}", file, expected_hash);
}
}
continue;
}