Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

```
Expand Down Expand Up @@ -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. |
Expand Down
81 changes: 71 additions & 10 deletions src/cli/cmd_configure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -34,29 +35,89 @@ 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<int>(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);

printf("[%.*s] Installed %s -> %s\n", static_cast<int>(variant_name.size()), variant_name.data(), config_path.string().c_str(), dest.string().c_str());
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<int>(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<int>(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<int>(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,
Expand Down
7 changes: 0 additions & 7 deletions src/cli/context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
117 changes: 117 additions & 0 deletions test/unit/test_e2e.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
// =============================================================================
Expand Down