diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3af3344 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "mesonbuild.configureOnOpen": false +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8b1cfe4..d179111 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -451,7 +451,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" [[package]] name = "depot" -version = "0.35.0" +version = "0.35.1" dependencies = [ "anyhow", "ar", @@ -479,7 +479,7 @@ dependencies = [ "serde_ignored", "sha1 0.11.0", "sha2 0.11.0", - "signal-hook 0.4.3", + "signal-hook 0.4.4", "suppaftp", "sys-mount", "tar", @@ -1913,9 +1913,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1972,9 +1972,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -2041,9 +2041,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b57709da74f9ff9f4a27dce9526eec25ca8407c45a7887243b031a58935fb8e" +checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" dependencies = [ "libc", "signal-hook-registry", @@ -2337,9 +2337,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", @@ -2352,27 +2352,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" @@ -3166,9 +3166,9 @@ dependencies = [ [[package]] name = "zip" -version = "8.4.0" +version = "8.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7756d0206d058333667493c4014f545f4b9603c4330ccd6d9b3f86dcab59f7d9" +checksum = "2726508a48f38dceb22b35ecbbd2430efe34ff05c62bd3285f965d7911b33464" dependencies = [ "aes", "bzip2", diff --git a/Cargo.toml b/Cargo.toml index a9e198d..322e94f 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "depot" -version = "0.35.0" +version = "0.35.1" edition = "2024" [lints.rust] @@ -16,18 +16,18 @@ git2 = "0.20.4" indicatif = "0.18.4" nix = { version = "0.31.2", features = ["user"] } rusqlite = "0.39.0" -semver = "1.0.27" +semver = "1.0.28" serde = { version = "1.0.228", features = ["derive"] } yaml-rust2 = "0.11.0" sha2 = "0.11.0" tar = "0.4.45" tempfile = "=3.27.0" thiserror = "2.0.18" -toml = "1.1.0+spec-1.1.0" +toml = "1.1.2" url = "2.5.8" walkdir = "2.5.0" xz2 = "0.1.7" -zip = "8.4.0" +zip = "8.5.0" zstd = { version = "0.13.3", features = ["zstdmt"] } inquire = "0.9.4" md5 = "0.8.0" @@ -45,7 +45,7 @@ b2sum-rust = "0.3.0" serde_ignored = "0.1.14" lz4_flex = "0.13.0" lzma-rust2 = "0.16.2" -signal-hook = "0.4.3" +signal-hook = "0.4.4" sha1 = "0.11.0" [dev-dependencies] diff --git a/contrib/depot.d/build.toml b/contrib/depot.d/build.toml index 82ec42f..6a055d9 100644 --- a/contrib/depot.d/build.toml +++ b/contrib/depot.d/build.toml @@ -1,28 +1,45 @@ [flags] -# Default build flags applied to all packages via /etc/depot.d/build.toml -# Compiler selection -cc = "gcc" +# Example /etc/depot.d/build.toml +# +# Drop-in build defaults. This file is a good place for policy that should +# apply everywhere without crowding the main /etc/depot.toml. -# Default CFLAGS +# Toolchain selection. +cc = "clang" +# cxx = "clang++" +# ar = "ar" +# ld = "ld.lld" + +# Compiler defaults. These are merged with package-level flags unless a package +# explicitly replaces them. cflags = ["-O2"] cflags += ["-pipe"] +# cxxflags = ["-O2", "-pipe"] -# Optional install directory overrides for supported builders -#bindir = "/usr/bin" -#libdir = "/usr/lib" -#sysconfdir = "/etc" - -# Default LDFLAGS +# Linker defaults. ldflags = ["-Wl,-O1"] -# Default LTOFLAGS and automatic injection into CFLAGS/CXXFLAGS/LDFLAGS. +# Link-time optimization policy. ltoflags = ["-flto=auto"] + # Rust-specific LTO flags appended only to RUSTFLAGS when use_lto is enabled. -#RUSTLTOFLAGS = ["-Clinker-plugin-lto"] +# rustltoflags = ["-Clinker-plugin-lto"] use_lto = true -# CARCH short name (can be overridden per-package) +# Architecture defaults. Packages may still override this when needed. carch = "x86_64" +# chost = "x86_64-sfg-linux-gnu" +# cbuild = "x86_64-sfg-linux-gnu" -# Example of appending per-package cflags: (will be exposed as build.cflags appends) +# Optional install directory overrides for supported builders. +# bindir = "/usr/bin" +# libdir = "/usr/lib" +# sysconfdir = "/etc" + +# Example of additive layering in depot.d: +# cflags += ["-fstack-clash-protection"] +# ldflags += ["-Wl,-z,relro", "-Wl,-z,now"] + +# Avoid host-specific tuning such as -march=native in shared system configs +# unless every package built on this machine is intended only for this machine. # cflags += ["-march=native"] diff --git a/contrib/depot.d/package.toml b/contrib/depot.d/package.toml index 90e9cff..7a88ac6 100644 --- a/contrib/depot.d/package.toml +++ b/contrib/depot.d/package.toml @@ -1,6 +1,19 @@ [package] -# System-wide package overrides. Example: set default provides or package metadata -# name = "" +# Example /etc/depot.d/package.toml +# +# System-wide package metadata overrides. Use this sparingly: this is best for +# policy defaults or local compatibility shims, not for rewriting package specs. -# Example: provide a virtual runtime package name to satisfy runtime deps +# Example fields you might override globally in a controlled environment: +# homepage = "https://mirror.example.org/packages" +# license = ["MIT"] + +# Example: provide a virtual package name to satisfy dependency expressions. +# This can be useful on systems with a local compatibility package. # provides = ["filesystem"] + +# Example: declare that local packages replace an older naming scheme. +# replaces = ["old-filesystem"] + +# Example: force a conflict relationship across locally maintained packages. +# conflicts = ["legacy-filesystem"] diff --git a/contrib/depot.d/repos.toml b/contrib/depot.d/repos.toml index 61dcd7f..5aece49 100644 --- a/contrib/depot.d/repos.toml +++ b/contrib/depot.d/repos.toml @@ -1,22 +1,38 @@ +# Example /etc/depot.d/repos.toml +# +# Repository configuration for source specs and binary package feeds. +# Lower priority numbers win first when multiple repos provide the same package. + [settings] +# Prefer binary packages when both a binary package and a source spec exist. +# Set this to false if you want source specs to be preferred by default. prefer_binary = true [source.vertex] +# Source repository used for spec lookup and source-build planning. url = "https://gitlab.com/vertex-linux/packages.git" enabled = true priority = 50 + +# Restrict scanning to selected subdirectories inside the source repo. subdirs = ["core", "extra"] +# Example: +# branch = "main" + [binary.core] +# Binary repository for core packages. url = "https://repo.sfgos.net/core" enabled = true priority = 10 repo_db = "repo.db.zst" [binary.core.arch.x86_64] +# Enable this repo for x86_64 systems. enabled = true [binary.extra] +# Secondary binary repository for additional packages. url = "https://repo.sfgos.net/extra" enabled = true priority = 20 @@ -24,3 +40,14 @@ repo_db = "repo.db.zst" [binary.extra.arch.x86_64] enabled = true + +# Additional examples: +# +# [binary.testing] +# url = "https://repo.example.org/testing" +# enabled = false +# priority = 5 +# repo_db = "repo.db.zst" +# +# [binary.testing.arch.x86_64] +# enabled = true diff --git a/contrib/depot.toml b/contrib/depot.toml index df7daa9..f144296 100644 --- a/contrib/depot.toml +++ b/contrib/depot.toml @@ -1,42 +1,83 @@ -# Example /etc/depot.toml - system-wide configuration for Depot +# Example /etc/depot.toml +# +# This file is the main system-wide configuration for Depot. +# Keep settings here for machine-wide defaults that should apply to every user. +# For smaller drop-in overrides, prefer adding files under /etc/depot.d/. +# +# Most values here act as defaults and can still be overridden: +# - per package in package specs +# - per invocation with CLI flags +# - per user in ~/.config/depot.toml [build] -# Installation prefix for package builds +# Installation prefix used by supported build systems when a package does not +# override it explicitly. For a normal system package build this is usually /usr. prefix = "/usr" -# Default compiler and archiver -cc = "gcc" + +# Default toolchain commands. These are passed through to builders that honor +# CC/CXX/AR and related variables. +cc = "clang" ar = "ar" +# Optional examples: +# cxx = "clang++" +# ld = "ld.lld" +# cpp = "cpp" +# strip = "strip" +# ranlib = "ranlib" + [build.flags] -# Basic compiler flags +# Baseline C compiler flags for packages that do not supply their own. +# Keep these conservative unless you are intentionally tuning for one machine. cflags = ["-O2"] -# Append additional flags (demonstrates += append support) + +# Appends are often clearer than rewriting the whole list. This demonstrates +# Depot's += support for layered config files. +# NOTE: this is NOT regular TOML syntax. The += operator is a special extension that Depot supports cflags += ["-g"] -# Optional install directory overrides for supported builders -#bindir = "/usr/bin" -#sbindir = "/usr/bin" -#libdir = "/usr/lib" -#libexecdir = "/usr/lib" -#sysconfdir = "/etc" -#localstatedir = "/var" -#sharedstatedir = "/var/lib" -#includedir = "/usr/include" -#datarootdir = "/usr/share" -#datadir = "/usr/share" -#mandir = "/usr/share/man" -#infodir = "/usr/share/info" - -# Linker flags +# Optional C++ and linker defaults: +# cxxflags = ["-O2"] ldflags = ["-Wl,-O1"] -# Target architecture short name (CARCH). Defaults to host arch if unset. +# Link-time optimization defaults. When use_lto is true, Depot will inject +# ltoflags into the appropriate compiler and linker flag sets automatically. +# ltoflags = ["-flto=auto"] +# use_lto = false + +# Target architecture short name (CARCH). If unset, Depot derives this from the +# current host system or active cross target. carch = "x86_64" -# Target triples (optional) -#chost = "x86_64-pc-linux-gnu" -#cbuild = "x86_64-pc-linux-gnu" +# Target triples are usually only needed for cross or multilib scenarios. +# chost = "x86_64-pc-linux-gnu" +# cbuild = "x86_64-pc-linux-gnu" + +# Optional install directory overrides for supported builders. Leave these at +# their defaults unless you are intentionally targeting a non-standard layout. +# bindir = "/usr/bin" +# sbindir = "/usr/bin" +# libdir = "/usr/lib" +# libexecdir = "/usr/lib" +# sysconfdir = "/etc" +# localstatedir = "/var" +# sharedstatedir = "/var/lib" +# includedir = "/usr/include" +# datarootdir = "/usr/share" +# datadir = "/usr/share" +# mandir = "/usr/share/man" +# infodir = "/usr/share/info" + +# Additional examples: +# makeflags = "-j8" +# env_vars = ["CCACHE_DIR=/var/cache/ccache"] +# passthrough_env = ["http_proxy", "https_proxy"] [install] -# Install test dependencies along with build/runtime dependencies. +# When true, Depot will also try to install declared test dependencies while +# resolving dependencies for source builds. test_deps = false + +# Example future-friendly place for install policy overrides: +# assume_yes = false +# keep_cache = true diff --git a/contrib/user.depot.toml.example b/contrib/user.depot.toml.example index 789178d..58988a5 100644 --- a/contrib/user.depot.toml.example +++ b/contrib/user.depot.toml.example @@ -1,18 +1,35 @@ # Example per-user configuration (~/.config/depot.toml) +# +# Use this file for local preferences that should not affect other users on the +# same machine. System-wide defaults still come from /etc/depot.toml and +# /etc/depot.d/*.toml unless you override them here. [build] +# A user-local prefix is useful when you are building software for your own +# account instead of assembling system packages. prefix = "/usr/local" + +# Personal toolchain preference. cc = "clang" +# cxx = "clang++" [build.flags] +# User-local compiler preferences. cflags = ["-O2"] cflags += ["-g"] carch = "x86_64" -[install] -test_deps = false +# Examples: +# cxxflags = ["-O2", "-g"] +# ldflags = ["-Wl,-O1"] +# makeflags = "-j4" -# Optional install directory overrides for supported builders -#bindir = "/usr/local/bin" -#sbindir = "/usr/local/sbin" -#libdir = "/usr/local/lib" +# Optional install directory overrides for supported builders. +# bindir = "/usr/local/bin" +# sbindir = "/usr/local/sbin" +# libdir = "/usr/local/lib" +# datadir = "/usr/local/share" + +[install] +# Install test dependencies automatically for your own source builds. +test_deps = false diff --git a/meson.build b/meson.build index e91a4b2..a718562 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project( 'depot', - version: '0.35.0', + version: '0.35.1', meson_version: '>=0.60.0', default_options: ['buildtype=release'], ) @@ -21,6 +21,7 @@ depot_custom_package = get_option('DEPOT_CUSTOM_PACKAGE') depot_python_package = get_option('DEPOT_PYTHON_PACKAGE') depot_rust_package = get_option('DEPOT_RUST_PACKAGE') depot_makefile_package = get_option('DEPOT_MAKEFILE_PACKAGE') +depot_development_package = get_option('DEPOT_DEVELOPMENT_PACKAGE') src_root = meson.project_source_root() build_root = meson.project_build_root() @@ -84,6 +85,7 @@ depot_bin = custom_target( depot_python_package, depot_rust_package, depot_makefile_package, + depot_development_package, '@OUTPUT@', ], console: true, @@ -128,6 +130,7 @@ test( depot_python_package, depot_rust_package, depot_makefile_package, + depot_development_package, ], suite: ['cargo'], timeout: 0, diff --git a/meson_options.txt b/meson_options.txt index dc397d0..589026b 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -58,3 +58,9 @@ option( value: '', description: 'Compile in the helper package to auto-install before makefile builds', ) +option( + 'DEPOT_DEVELOPMENT_PACKAGE', + type: 'string', + value: 'development-base', + description: 'Compile in the package that must already be installed before any source build runs', +) diff --git a/src/build_options.rs b/src/build_options.rs index af6def3..1b806d1 100644 --- a/src/build_options.rs +++ b/src/build_options.rs @@ -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 { match value.trim().to_ascii_lowercase().as_str() { @@ -63,9 +64,20 @@ pub(crate) fn requested_build_tool_package(build_type: BuildType) -> Option &'static str { + DEPOT_DEVELOPMENT_PACKAGE_OPTION +} + +pub(crate) fn requested_development_package() -> Option { + 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"); + } } diff --git a/src/builder/autotools.rs b/src/builder/autotools.rs index cdfbbb8..f5efca4 100755 --- a/src/builder/autotools.rs +++ b/src/builder/autotools.rs @@ -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); } diff --git a/src/builder/cmake.rs b/src/builder/cmake.rs index 38f09fd..6649447 100755 --- a/src/builder/cmake.rs +++ b/src/builder/cmake.rs @@ -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); } diff --git a/src/builder/custom.rs b/src/builder/custom.rs index 7bcc180..3e4c270 100755 --- a/src/builder/custom.rs +++ b/src/builder/custom.rs @@ -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 { || contents.contains("install_")) } -fn build_function_mode_command( - spec: &PackageSpec, - destdir: &Path, - build_script: &Path, -) -> Result { +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()?; diff --git a/src/builder/meson.rs b/src/builder/meson.rs index 2fb8fc0..aeafa73 100755 --- a/src/builder/meson.rs +++ b/src/builder/meson.rs @@ -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); } diff --git a/src/builder/mod.rs b/src/builder/mod.rs index 79a962e..3537ab0 100755 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -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> { crate::build_options::requested_static_build() } -pub(crate) fn static_build_args_for(build_type: BuildType) -> Result> { - let Some(enabled) = requested_static_build()? else { - return Ok(Vec::new()); +fn static_build_args_for_request( + build_type: BuildType, + requested_static: Option, + no_delete_static: bool, +) -> Vec { + 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 if enabled { "static" } else { "dynamic" } )], _ => Vec::new(), - }; + } +} - Ok(args) +pub(crate) fn static_build_args_for( + build_type: BuildType, + flags: &BuildFlags, +) -> Result> { + 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 &'static str { + crate::build_options::development_package_option() +} + +pub(crate) fn requested_development_package() -> Option { + 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()); diff --git a/src/builder/perl.rs b/src/builder/perl.rs index 6133b27..7f2490d 100644 --- a/src/builder/perl.rs +++ b/src/builder/perl.rs @@ -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); diff --git a/src/commands.rs b/src/commands.rs index 315187b..5eb7677 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -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 { .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 { + 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( diff --git a/src/commands/build_cmd.rs b/src/commands/build_cmd.rs index 21922d8..d0849f3 100644 --- a/src/commands/build_cmd.rs +++ b/src/commands/build_cmd.rs @@ -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()?; diff --git a/src/commands/build_cmd/support.rs b/src/commands/build_cmd/support.rs index eafe472..6278740 100644 --- a/src/commands/build_cmd/support.rs +++ b/src/commands/build_cmd/support.rs @@ -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(()) + } +} diff --git a/src/commands/tests.rs b/src/commands/tests.rs index 94198c5..f2d9b29 100644 --- a/src/commands/tests.rs +++ b/src/commands/tests.rs @@ -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(); diff --git a/src/source/mod.rs b/src/source/mod.rs index b967751..01bb493 100755 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -62,11 +62,42 @@ fn manual_url_entries(manual: &crate::package::ManualSource) -> Vec { /// 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; } diff --git a/tools/meson/cargo-build.sh b/tools/meson/cargo-build.sh index 8c795b0..920c565 100644 --- a/tools/meson/cargo-build.sh +++ b/tools/meson/cargo-build.sh @@ -1,8 +1,8 @@ #!/bin/sh set -eu -if [ "$#" -ne 15 ]; then - echo "usage: $0 " >&2 +if [ "$#" -ne 16 ]; then + echo "usage: $0 " >&2 exit 2 fi @@ -20,7 +20,8 @@ custom_pkg="${11}" python_pkg="${12}" rust_pkg="${13}" makefile_pkg="${14}" -output="${15}" +development_pkg="${15}" +output="${16}" cargo_home="$build_root/cargo-home" cargo_target_dir="$build_root/cargo-target" @@ -38,6 +39,7 @@ export DEPOT_CUSTOM_PACKAGE="$custom_pkg" export DEPOT_PYTHON_PACKAGE="$python_pkg" export DEPOT_RUST_PACKAGE="$rust_pkg" export DEPOT_MAKEFILE_PACKAGE="$makefile_pkg" +export DEPOT_DEVELOPMENT_PACKAGE="$development_pkg" if [ "$release_flag" = "1" ]; then "$cargo_bin" build --locked --manifest-path "$src_root/Cargo.toml" --release diff --git a/tools/meson/cargo-test.sh b/tools/meson/cargo-test.sh index 91185dd..55193b2 100644 --- a/tools/meson/cargo-test.sh +++ b/tools/meson/cargo-test.sh @@ -1,8 +1,8 @@ #!/bin/sh set -eu -if [ "$#" -ne 14 ]; then - echo "usage: $0 " >&2 +if [ "$#" -ne 15 ]; then + echo "usage: $0 " >&2 exit 2 fi @@ -20,6 +20,7 @@ custom_pkg="${11}" python_pkg="${12}" rust_pkg="${13}" makefile_pkg="${14}" +development_pkg="${15}" cargo_home="$build_root/cargo-home" cargo_target_dir="$build_root/cargo-target" @@ -37,6 +38,7 @@ export DEPOT_CUSTOM_PACKAGE="$custom_pkg" export DEPOT_PYTHON_PACKAGE="$python_pkg" export DEPOT_RUST_PACKAGE="$rust_pkg" export DEPOT_MAKEFILE_PACKAGE="$makefile_pkg" +export DEPOT_DEVELOPMENT_PACKAGE="$development_pkg" if [ "$release_flag" = "1" ]; then exec "$cargo_bin" test --locked --manifest-path "$src_root/Cargo.toml" --profile "$profile"