From fa65b0703b222201ece72f9c42e24ad909fd6741 Mon Sep 17 00:00:00 2001 From: Mura Li <2606021+typeless@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:25:27 +0800 Subject: [PATCH] Copy subdir tup.config files during configure for scoped configs Configure now runs a unified three-step flow: 1. Install root config (if --config specified) 2. Copy subdir tup.config files from config root to build tree 3. Run config-generating rules (unchanged) Step 2 enables per-component scoped configs: each subdirectory ships a tup.config alongside its Tupfile. At configure time, these are installed into the build tree where scoped config merging picks them up during the build. For in-tree builds (config_root == output_root), step 2 is a no-op. Also removes the false-positive variant skip from discover_tupfile_dirs that treated any directory with both Tupfile and tup.config as a variant. Variant directories don't contain Tupfiles in normal usage. Co-Authored-By: Claude Opus 4.6 --- docs/reference.md | 38 ++++++++++--- src/cli/cmd_configure.cpp | 81 ++++++++++++++++++++++---- src/cli/context.cpp | 7 --- test/unit/test_e2e.cpp | 117 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 25 deletions(-) diff --git a/docs/reference.md b/docs/reference.md index d54d280..2fe38fc 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -258,32 +258,54 @@ putup # Pass 2: Build with generated configs ``` **How it works:** -1. Parses all Tupfiles using root `tup.config` only -2. Identifies rules where any output ends with `tup.config` -3. Executes only those rules (plus their dependencies) -4. Does not write to `.pup/index` (avoids conflict with subsequent build) +1. Installs root config if `--config` is specified +2. Copies subdir `tup.config` files from config root to build tree (out-of-tree only) +3. Parses all Tupfiles using root `tup.config` only +4. Identifies rules where any output ends with `tup.config` +5. Executes only those rules (plus their dependencies) +6. Does not write to `.pup/index` (avoids conflict with subsequent build) **Relevant Options:** - `-v` - Verbose output - `-k` - Continue after failures - `-n` - Dry-run: show what would execute - `-B DIR` - Specify build directory (created automatically if it doesn't exist) -- `-c, --config FILE` - Use FILE as tup.config directly (skip config-generating rules) +- `-c, --config FILE` - Install FILE as root tup.config before running config rules **Note:** The `-B` flag creates the output directory if needed. After configure runs, the directory contains `tup.config` which marks it as a variant for subsequent builds. If no config-generating rules exist, an empty `tup.config` is created automatically. The `.pup/` index is NOT created during configure (it's created on first build). **Using --config for pre-made configs:** -The `--config` option copies an existing config file directly to the output directory, skipping config-generating rules entirely. Useful for: +The `--config` option copies an existing config file to the output directory as the root `tup.config`. It then continues with steps 2–6 above: copying subdir configs and running any config-generating rules. Useful for: - Cross-compilation with pre-made toolchain configs - CI/CD where configs are externally managed -- Quick testing with different configurations +- Mixed workflows with a static root config + auto-generated subdir configs ```bash putup configure -B build --config configs/arm-cross.config putup configure -B build-debug -c debug.config ``` +**Subdir tup.config copying (step 2):** + +For out-of-tree builds (`config_root != output_root`), configure automatically copies any `tup.config` files found in subdirectories of the config root to the corresponding locations in the build tree. The root-level `tup.config` is excluded (handled by `--config` or config-generating rules). + +This enables per-component scoped configs: each subdirectory ships a `tup.config` alongside its Tupfile. At configure time, these are installed into the build tree where scoped config merging (§6.1) picks them up during the build. + +```bash +# Config root has per-component configs: +# gmp/tup.config, mpfr/tup.config, mpc/tup.config +# +# configure installs root config AND copies subdir configs: +putup configure --config configs/toolchain.config -C . -S ../src -B ../build +# → ../build/tup.config (from --config) +# → ../build/gmp/tup.config (copied from gmp/tup.config) +# → ../build/mpfr/tup.config (copied from mpfr/tup.config) +# → ../build/mpc/tup.config (copied from mpc/tup.config) +``` + +For in-tree builds (`config_root == output_root`), step 2 is a no-op — the configs are already in place. + **Important:** You must run `putup configure` before `putup build`. If you skip the configure step, `putup build` will error: ``` @@ -506,7 +528,7 @@ Estimated savings: 92% (instruction + operands vs full strings) | `-S DIR` | | Source directory. Overrides auto-detection. | | `-C DIR` | `--config-dir` | Config directory (where Tupfiles live). | | `-B DIR` | | Build/output directory (can use multiple times). | -| `-c FILE` | `--config` | Use FILE as tup.config (configure command only). | +| `-c FILE` | `--config` | Install FILE as root tup.config (configure command only). | | `-A` | `--all` | Full project build, ignoring cwd scoping. | | `-a` | `--all-deps` | Include upstream deps in scoped builds. | | | `--stat` | Print build statistics after completion. | diff --git a/src/cli/cmd_configure.cpp b/src/cli/cmd_configure.cpp index ca78d1e..6395589 100644 --- a/src/cli/cmd_configure.cpp +++ b/src/cli/cmd_configure.cpp @@ -20,11 +20,12 @@ namespace pup::cli { namespace { auto install_config_file( - Options const& opts, + ProjectLayout const& layout, + std::string const& config_file, std::string_view variant_name ) -> int { - auto config_path = std::filesystem::path { opts.config_file }; + auto config_path = std::filesystem::path { config_file }; if (config_path.is_relative()) { config_path = std::filesystem::current_path() / config_path; } @@ -34,13 +35,7 @@ auto install_config_file( return EXIT_FAILURE; } - auto layout = discover_layout(make_layout_options(opts)); - if (!layout) { - fprintf(stderr, "[%.*s] Error: %s\n", static_cast(variant_name.size()), variant_name.data(), layout.error().message.c_str()); - return EXIT_FAILURE; - } - - auto dest = layout->output_root / "tup.config"; + auto dest = layout.output_root / "tup.config"; std::filesystem::create_directories(dest.parent_path()); std::filesystem::copy_file(config_path, dest, std::filesystem::copy_options::overwrite_existing); @@ -48,15 +43,81 @@ auto install_config_file( return EXIT_SUCCESS; } +auto install_source_configs( + ProjectLayout const& layout, + std::string_view variant_name, + bool verbose +) -> int +{ + if (layout.config_root == layout.output_root) { + return 0; + } + + auto config_canonical = std::filesystem::weakly_canonical(layout.config_root); + auto output_canonical = std::filesystem::weakly_canonical(layout.output_root); + auto count = 0; + auto ec = std::error_code {}; + + for (auto it = std::filesystem::recursive_directory_iterator(config_canonical, ec); + it != std::filesystem::recursive_directory_iterator(); + ++it) { + if (ec) { + break; + } + + // Skip the output tree if it lives inside config_root (e.g., -B build) + if (it->is_directory() && it->path() == output_canonical) { + it.disable_recursion_pending(); + continue; + } + + if (!it->is_regular_file() || it->path().filename() != "tup.config") { + continue; + } + + auto rel = std::filesystem::relative(it->path(), config_canonical); + if (rel == "tup.config") { + continue; + } + + auto dest = layout.output_root / rel; + std::filesystem::create_directories(dest.parent_path()); + std::filesystem::copy_file(it->path(), dest, std::filesystem::copy_options::overwrite_existing); + if (verbose) { + printf("[%.*s] Copied %s -> %s\n", static_cast(variant_name.size()), variant_name.data(), it->path().string().c_str(), dest.string().c_str()); + } + ++count; + } + if (count > 0) { + printf("[%.*s] Installed %d source config(s)\n", static_cast(variant_name.size()), variant_name.data(), count); + } + return count; +} + auto configure_single_variant( Options const& opts, std::string_view variant_name ) -> int { + // Discover layout once for steps 1 & 2 + auto layout = discover_layout(make_layout_options(opts)); + if (!layout) { + fprintf(stderr, "[%.*s] Error: %s\n", static_cast(variant_name.size()), variant_name.data(), layout.error().message.c_str()); + return EXIT_FAILURE; + } + + // Step 1: Install root config if --config specified if (!opts.config_file.empty()) { - return install_config_file(opts, variant_name); + auto rc = install_config_file(*layout, opts.config_file, variant_name); + if (rc != EXIT_SUCCESS) { + return rc; + } } + // Step 2: Copy source subdir tup.config files to build tree + install_source_configs(*layout, variant_name, opts.verbose); + + // Step 3: Run config-generating rules auto ctx_opts = BuildContextOptions { .verbose = opts.verbose, .keep_going = opts.keep_going, diff --git a/src/cli/context.cpp b/src/cli/context.cpp index ddc1dce..6c915fe 100644 --- a/src/cli/context.cpp +++ b/src/cli/context.cpp @@ -195,13 +195,6 @@ auto discover_tupfile_dirs( } auto dir = std::filesystem::path { entry.path().parent_path() }; - - // Skip variant directories (have tup.config but are not the source root) - // The source root may have both Tupfile and tup.config - if (dir != root && std::filesystem::exists(dir / "tup.config")) { - continue; - } - auto dir_rel = std::filesystem::relative(dir, root); dirs.insert(normalize_to_dot(dir_rel)); } diff --git a/test/unit/test_e2e.cpp b/test/unit/test_e2e.cpp index 1e6242d..b5bf814 100644 --- a/test/unit/test_e2e.cpp +++ b/test/unit/test_e2e.cpp @@ -3521,6 +3521,123 @@ SCENARIO("configure with --config followed by build works", "[e2e][configure][bu } } +SCENARIO("configure copies source subdir configs to build tree", "[e2e][configure]") +{ + GIVEN("a project with a subdir tup.config") + { + auto f = E2EFixture { "simple_c" }; + f.mkdir("sub"); + f.write_file("sub/tup.config", "CONFIG_SUB_VAR=from_sub\n"); + f.write_file("root.config", "CONFIG_ROOT_VAR=from_root\n"); + REQUIRE(f.init().success()); + + WHEN("configure is run with --config and -B") + { + auto result = f.pup({ "configure", "--config", "root.config", "-B", "build" }); + + THEN("root config is installed to build/tup.config") + { + INFO("stdout: " << result.stdout_output); + INFO("stderr: " << result.stderr_output); + REQUIRE(result.success()); + REQUIRE(f.exists("build/tup.config")); + auto content = f.read_file("build/tup.config"); + REQUIRE(content.find("CONFIG_ROOT_VAR=from_root") != std::string::npos); + } + + THEN("subdir config is copied to build/sub/tup.config") + { + REQUIRE(f.exists("build/sub/tup.config")); + auto content = f.read_file("build/sub/tup.config"); + REQUIRE(content.find("CONFIG_SUB_VAR=from_sub") != std::string::npos); + } + } + } +} + +SCENARIO("configure --config + subdir configs + build uses scoped merge", "[e2e][configure][scoped-config]") +{ + GIVEN("a project with root config and subdir tup.config in source tree") + { + auto f = E2EFixture { "scoped_config" }; + f.write_file("sub/tup.config", "CONFIG_SUB_VAR=from_sub\n"); + f.write_file("root.config", "CONFIG_ROOT_VAR=from_root\n"); + REQUIRE(f.init().success()); + + auto configure_result = f.pup({ "configure", "--config", "root.config", "-B", "build" }); + REQUIRE(configure_result.success()); + + WHEN("build is run") + { + auto result = f.build({ "-B", "build" }); + + THEN("sub/ sees both root and subdir vars via scoped merge") + { + INFO("stdout: " << result.stdout_output); + INFO("stderr: " << result.stderr_output); + REQUIRE(result.success()); + REQUIRE(f.read_file("build/sub/sub.txt") == "from_sub\n"); + REQUIRE(f.read_file("build/sub/root_from_sub.txt") == "from_root\n"); + } + } + } +} + +SCENARIO("configure skips subdir config copy for in-tree", "[e2e][configure]") +{ + GIVEN("a project with -C matching output root") + { + auto f = E2EFixture { "scoped_config" }; + f.write_file("tup.config", "CONFIG_ROOT_VAR=from_root\n"); + REQUIRE(f.init().success()); + + WHEN("configure is run without -B") + { + auto result = f.pup({ "configure" }); + + THEN("configure succeeds without copying configs") + { + INFO("stdout: " << result.stdout_output); + INFO("stderr: " << result.stderr_output); + REQUIRE(result.success()); + REQUIRE(result.stdout_output.find("source config") == std::string::npos); + } + } + } +} + +SCENARIO("configure handles mixed static + auto-gen configs", "[e2e][configure]") +{ + GIVEN("a project with a static subdir config and auto-gen config rules") + { + auto f = E2EFixture { "configure_cmd" }; + f.mkdir("build"); + f.write_file("build/tup.config", "CONFIG_MACHINE=board-xyz\n"); + f.write_file("sub/tup.config", "CONFIG_STATIC_VAR=static_value\n"); + REQUIRE(f.init().success()); + + WHEN("configure is run with -B") + { + auto result = f.pup({ "configure", "-B", "build" }); + + THEN("auto-gen config is produced") + { + INFO("stdout: " << result.stdout_output); + INFO("stderr: " << result.stderr_output); + REQUIRE(result.success()); + REQUIRE(f.exists("build/configs/tup.config")); + } + + THEN("static subdir config is copied") + { + REQUIRE(f.exists("build/sub/tup.config")); + auto content = f.read_file("build/sub/tup.config"); + REQUIRE(content.find("CONFIG_STATIC_VAR=static_value") != std::string::npos); + } + } + } +} + // ============================================================================= // Duplicate Output Detection Tests // =============================================================================