diff --git a/docs/reference.md b/docs/reference.md index e46ba77..37400a2 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -920,10 +920,11 @@ include config.tup include ../common/rules.tup ``` -**`include_rules`** - Include Tuprules.tup from current and parent directories: +**`include_rules`** - Include all `Tuprules.tup` files from the project root down to the current directory, in root-first order. Each directory in the path from root to the Tupfile's directory is checked; gaps (directories without a `Tuprules.tup`) are silently skipped. For `sub/deep/Tupfile`, this is equivalent to: ```tup -include_rules -# Searches upward for Tuprules.tup files +include ../../Tuprules.tup # root +include ../Tuprules.tup # sub/ (if it exists) +include Tuprules.tup # sub/deep/ (if it exists) ``` **`export`** - Export variable to command environment: @@ -1529,6 +1530,67 @@ When `-C` is not specified: 2. If output has `Tupfile.ini` → use output as config root (two-tree) 3. Otherwise → use source as config root (simple projects) +### 7.6 Self-Contained Library Convention + +Large projects often contain multiple libraries that share a build (GMP + MPFR + MPC inside GCC, for example). This convention makes each library buildable on its own while staying composable in the larger project. + +**The pattern:** + +1. **Root `Tuprules.tup`** sets the project layout — toolchain variables and directory names for each library: + +```tup +# Root Tuprules.tup +S = $(TUP_CWD) +B = $(TUP_VARIANT_OUTPUTDIR)/$(S) +CC = @(CC) +AR = @(AR) + +GMP_DIR = gmp +MPFR_DIR = mpfr +``` + +2. **Each library's `Tuprules.tup`** provides `?=` defaults for standalone use. In composed mode, root's assignments win and the defaults are no-ops: + +```tup +# mpfr/Tuprules.tup +S ?= $(TUP_CWD) +B ?= $(TUP_VARIANT_OUTPUTDIR)/$(S) +CC ?= gcc +AR ?= ar +GMP_DIR ?= ../gmp +MPFR_DIR ?= . + +CFLAGS = -O2 -DHAVE_CONFIG_H +CFLAGS += -I$(S)/$(MPFR_DIR)/src +CFLAGS += -I$(S)/$(GMP_DIR) + +!cc = | $(S)/$(GMP_DIR)/ |> ^ CC %b^ $(CC) $(CFLAGS) -c %f -o %o |> %B.o +``` + +3. **Tupfiles** use unprefixed names (`CFLAGS`, `!cc`) — each library's `Tuprules.tup` is only visible to its own subtree via `include_rules`. + +**Why directory names are prefixed (`GMP_DIR`, not `DIR`):** + +A library that depends on another needs to reference both directories in the same file. MPC's `Tuprules.tup` has `-I$(S)/$(GMP_DIR)`, `-I$(S)/$(MPFR_DIR)/src`, and `-I$(S)/$(MPC_DIR)/src` — three different paths that need three different names. The root also sets all of them in one shared scope, so a single `DIR` variable would collide. + +CFLAGS and bang macros (`!cc`, `!gen-config`) don't need prefixes because each library's `Tuprules.tup` is only included by Tupfiles in its own subtree — there's no shared scope where they could collide. + +**How `include_rules` enables this:** + +`include_rules` includes every `Tuprules.tup` from the project root down to the current directory. For `mpfr/src/Tupfile`, this means root's `Tuprules.tup` runs first (setting `S`, `GMP_DIR = gmp`, `MPFR_DIR = mpfr`), then `mpfr/Tuprules.tup` runs second (its `?=` defaults are no-ops, but it defines `CFLAGS` and `!cc`). + +**Standalone vs composed paths:** + +``` +# Composed (from mpfr/src/, root is ../..) +S = ../.. GMP_DIR = gmp → $(S)/$(GMP_DIR) = ../../gmp ✓ + +# Standalone (mpfr is root, from mpfr/src/) +S = .. GMP_DIR = ../gmp → $(S)/$(GMP_DIR) = ../../gmp ✓ +``` + +See `examples/gcc/` for a complete working example with three interdependent libraries. + ## 8. Implicit Dependencies Header files included by C/C++ sources aren't listed in Tupfiles, but changes to them should trigger rebuilds. Putup tracks these "implicit dependencies" automatically. diff --git a/src/graph/builder.cpp b/src/graph/builder.cpp index 8359861..e1b944d 100644 --- a/src/graph/builder.cpp +++ b/src/graph/builder.cpp @@ -511,27 +511,37 @@ auto create_command_node( std::string const& display ) -> Result; -/// Search up the directory tree for Tuprules.tup -/// Returns empty path if not found -auto find_tuprules_file( +/// Collect all Tuprules.tup files from root to start_dir (root-first order). +/// Per tup semantics, include_rules includes every Tuprules.tup from the +/// project root down to the current directory. Gaps are allowed. +auto find_tuprules_files( fs::path const& start_dir, fs::path const& root -) -> fs::path +) -> std::vector { + auto dirs = std::vector {}; auto search_dir = start_dir; while (search_dir >= root) { - auto tuprules = fs::path { search_dir / "Tuprules.tup" }; - if (fs::exists(tuprules)) { - return tuprules; - } + dirs.push_back(search_dir); if (search_dir == root) { break; } search_dir = search_dir.parent_path(); } - return {}; + // Reverse to root-first order + std::reverse(dirs.begin(), dirs.end()); + + auto results = std::vector {}; + for (auto const& dir : dirs) { + auto tuprules = dir / "Tuprules.tup"; + if (fs::exists(tuprules)) { + results.push_back(tuprules); + } + } + + return results; } /// Resolve an explicit include path (not include_rules) @@ -1120,47 +1130,25 @@ auto process_conditional( return {}; } -auto process_include( +auto include_single_file( BuilderContext& ctx, BuilderState& state, - parser::Include const& inc + fs::path const& include_root, + std::string const& include_path, + bool is_rules ) -> Result { - // Include files (Tuprules.tup, etc.) live in config_root (same as Tupfiles) - // Use config_root if set, otherwise fall back to source_root for traditional builds - auto const& include_root = ctx.options.config_root.empty() ? ctx.options.source_root : ctx.options.config_root; - - // Find the include file path - auto include_path = std::string {}; - if (inc.is_rules) { - auto tuprules = find_tuprules_file(include_root / ctx.current_dir, include_root); - if (tuprules.empty()) { - return {}; // No Tuprules.tup found, silently continue - } - include_path = tuprules.generic_string(); - } else { - auto resolved = resolve_include_path(ctx, include_root, inc.path); - if (!resolved) { - return pup::unexpected(resolved.error()); - } - include_path = resolved->generic_string(); - } - - // Prevent infinite recursion if (ctx.included_files.contains(include_path)) { return {}; } ctx.included_files.insert(include_path); - // Add included file to sticky_sources for dependency tracking - // Included files live in config_root, so use include_root for relative path auto inc_rel = fs::relative(include_path, include_root).generic_string(); auto inc_node_result = get_or_create_file_node(ctx, inc_rel, NodeType::File); if (inc_node_result) { ctx.sticky_sources.push_back(*inc_node_result); } - // Read the include file auto file = std::ifstream { include_path }; if (!file) { return make_error(ErrorCode::IoError, "Cannot open include file: " + include_path); @@ -1170,7 +1158,6 @@ auto process_include( ss << file.rdbuf(); auto source = std::string { ss.str() }; - // Parse the include file auto parse_result = parser::parse_tupfile(source, include_path); if (!parse_result.success()) { for (auto const& err : parse_result.errors) { @@ -1179,43 +1166,62 @@ auto process_include( return make_error(ErrorCode::ParseError, "Parse error in include file: " + include_path); } - // For include_rules, temporarily set TUP_CWD to the relative path from - // the Tupfile directory back to the Tuprules.tup directory. This allows - // patterns like ROOT = $(TUP_CWD) to work correctly. auto old_tup_cwd = std::string {}; - if (inc.is_rules && ctx.eval) { + if (is_rules && ctx.eval) { old_tup_cwd = ctx.eval->tup_cwd; - // Compute relative path from Tupfile directory to include file's directory auto include_dir = fs::path { include_path }.parent_path(); auto rel_path = fs::relative(include_dir, include_root / ctx.current_dir); ctx.eval->tup_cwd = rel_path.empty() ? "." : rel_path.generic_string(); } - // Save and update current_file for variable tracking callback auto old_current_file = ctx.current_file; ctx.current_file = include_path; - // Process statements from the included file for (auto const& stmt : parse_result.tupfile.statements) { auto result = Result { process_statement(ctx, state, *stmt) }; if (!result) { ctx.current_file = old_current_file; - if (inc.is_rules && ctx.eval) { + if (is_rules && ctx.eval) { ctx.eval->tup_cwd = old_tup_cwd; } return pup::unexpected(result.error()); } } - // Restore original current_file and TUP_CWD ctx.current_file = old_current_file; - if (inc.is_rules && ctx.eval) { + if (is_rules && ctx.eval) { ctx.eval->tup_cwd = old_tup_cwd; } return {}; } +auto process_include( + BuilderContext& ctx, + BuilderState& state, + parser::Include const& inc +) -> Result +{ + auto const& include_root = ctx.options.config_root.empty() ? ctx.options.source_root : ctx.options.config_root; + + if (inc.is_rules) { + auto tuprules_files = find_tuprules_files(include_root / ctx.current_dir, include_root); + for (auto const& tuprules : tuprules_files) { + auto result = include_single_file(ctx, state, include_root, tuprules.generic_string(), true); + if (!result) { + return pup::unexpected(result.error()); + } + } + return {}; + } + + auto resolved = resolve_include_path(ctx, include_root, inc.path); + if (!resolved) { + return pup::unexpected(resolved.error()); + } + return include_single_file(ctx, state, include_root, resolved->generic_string(), false); +} + auto process_import( BuilderContext& ctx, BuilderState& state, diff --git a/test/e2e/fixtures/include_rules_nested/Tupfile.ini b/test/e2e/fixtures/include_rules_nested/Tupfile.ini new file mode 100644 index 0000000..e69de29 diff --git a/test/e2e/fixtures/include_rules_nested/Tuprules.tup b/test/e2e/fixtures/include_rules_nested/Tuprules.tup new file mode 100644 index 0000000..bc1893f --- /dev/null +++ b/test/e2e/fixtures/include_rules_nested/Tuprules.tup @@ -0,0 +1,2 @@ +ROOT = $(TUP_CWD) +CC ?= gcc diff --git a/test/e2e/fixtures/include_rules_nested/sub/Tupfile b/test/e2e/fixtures/include_rules_nested/sub/Tupfile new file mode 100644 index 0000000..2b20884 --- /dev/null +++ b/test/e2e/fixtures/include_rules_nested/sub/Tupfile @@ -0,0 +1,2 @@ +include_rules +: |> echo $(ROOT) $(CC) $(SUBVAR) > %o |> result.txt diff --git a/test/e2e/fixtures/include_rules_nested/sub/Tuprules.tup b/test/e2e/fixtures/include_rules_nested/sub/Tuprules.tup new file mode 100644 index 0000000..04de6eb --- /dev/null +++ b/test/e2e/fixtures/include_rules_nested/sub/Tuprules.tup @@ -0,0 +1,2 @@ +SUBVAR = from_sub +CC ?= clang diff --git a/test/unit/test_e2e.cpp b/test/unit/test_e2e.cpp index edbe2b3..902799f 100644 --- a/test/unit/test_e2e.cpp +++ b/test/unit/test_e2e.cpp @@ -4868,6 +4868,37 @@ SCENARIO("Rules with empty input patterns are skipped", "[e2e][empty-input]") // Incremental Build Command String Mismatch Tests // ============================================================================= +SCENARIO("include_rules includes all Tuprules.tup from root to leaf", "[e2e][build]") +{ + GIVEN("a project with Tuprules.tup at root and in a subdirectory") + { + auto f = E2EFixture { "include_rules_nested" }; + REQUIRE(f.init().success()); + + WHEN("building the subdirectory Tupfile") + { + auto result = f.build(); + + THEN("both root and sub Tuprules.tup are included") + { + INFO("stdout: " << result.stdout_output); + INFO("stderr: " << result.stderr_output); + REQUIRE(result.success()); + + auto content = f.read_file("sub/result.txt"); + INFO("result.txt: " << content); + + // ROOT comes from root Tuprules.tup (TUP_CWD = .. from sub/) + REQUIRE(content.find("..") != std::string::npos); + // CC = gcc (root sets it first, sub's ?= doesn't override) + REQUIRE(content.find("gcc") != std::string::npos); + // SUBVAR comes from sub/Tuprules.tup + REQUIRE(content.find("from_sub") != std::string::npos); + } + } + } +} + SCENARIO("Sibling directory inputs work with incremental variant builds", "[e2e][incremental][variant]") { // This tests the command string matching between graph and index.