diff --git a/spec/requirements/change-detection.ears.md b/spec/requirements/change-detection.ears.md new file mode 100644 index 00000000..b880d487 --- /dev/null +++ b/spec/requirements/change-detection.ears.md @@ -0,0 +1,40 @@ +# Change detection — requirements + +- area: change-detection +- required-legs: none + +The subject here is the predicate a build applies to a file it already knows about: given the +file on disk and what the previous build recorded about it, has it changed? Only the predicate +is in scope. Writing the previous value down and turning a difference into work for the command +and its consumers are the record and route obligations of `command-record`, which owns them; this +area declares `required-legs: none` because what is left once those are removed is a function of +one file and one recorded value, not state carried across builds. + +The predicate is content, not timestamp. That is the deliberate deviation from upstream tup, and +it decides the two ways the predicate can fail a user: calling a file changed that is not, which +costs a rebuild, and failing to read the file at all, which leaves the answer unknown and must be +said out loud rather than resolved silently in either direction. + +See `README.md` for the format and the rules that apply to every area. + +--- + +## Group: predicate + +What makes a known file changed. + +### REQ-CHANGE-CONTENT + +- conformance: deliberate-deviation +- reference: upstream `tup_file_mod_mtime` marks a file modified whenever `MTIME_EQ` fails against the recorded timestamp and never compares content; putup hashes instead, so a touch costs nothing +- discharge: test "Scenario: Touch does not trigger unnecessary rebuild" + +When a file's modification time changes and its content does not, putup shall treat that file as +unchanged. + +### REQ-CHANGE-HASH-FAILURE + +- conformance: putup-only +- discharge: test "Scenario: A file that cannot be hashed is named in a warning" + +If putup cannot read a file whose content it must hash, then putup shall warn and name that file. diff --git a/spec/requirements/command-record.ears.md b/spec/requirements/command-record.ears.md index 7c4747e8..6435de67 100644 --- a/spec/requirements/command-record.ears.md +++ b/spec/requirements/command-record.ears.md @@ -92,6 +92,7 @@ performed. - conformance: tup-conformant - reference: tup `delete_files` (updater.c) aborts the update when `delete_file` fails, and retires the node with `tup_del_id_force` only after the unlink has succeeded - discharge: test "Scenario: A stale output that cannot be deleted fails the build and keeps its record" +- discharge: test "Scenario: A stale output that cannot even be queried keeps its record" If putup cannot delete a stale output, then putup shall fail the build and keep the record that names that output. @@ -235,6 +236,16 @@ commands that consume its outputs. While a build is restricted to a target or a scope, putup shall preserve the recorded state of every command outside that scope. +### REQ-EXIT-ABORT + +- leg: invariant +- conformance: unclassified +- reference: upstream mechanism not read; same class as #187, stated for a build that ended before the command ran rather than for a command that ran and failed (#304) +- discharge: test "Scenario: A build aborted before a command could run does not record it as done" + +If a build ends before a command it scheduled has exited zero, then putup shall keep that +command recorded as needing to run. + --- ## Group: input-set @@ -401,6 +412,7 @@ scheduled. - reference: upstream keeps discovered dependencies in its dependency graph, so ordering by them is not a putup invention; no line citation read yet - discharge: test "Scenario: A discovered dependency orders its consumer on a later build" - discharge: test "Scenario: A recorded discovery that the rules now contradict does not stall the build" +- discharge: test "Scenario: A discovery whose producing rule is gone orders nothing" When a build schedules both a command and one that produced a file that command was recorded as having read, putup shall run the producer first unless the rules order the two the other way. @@ -429,6 +441,47 @@ whatever ordering an earlier build recorded. When a change reaches a command that produces a file another command was recorded as having read, putup shall schedule the reader as well. +### REQ-IMPL-NORACE + +- leg: invariant +- conformance: unclassified +- reference: bounds REQ-IMPL-RACE; upstream orders by its discovered dependencies and so has no unordered pair left to mark, as REQ-IMPL-RACE records; no line citation read yet (#274) +- discharge: test "Scenario: A consumer ordered through a sibling output is not taxed for discovering the other" + +If the ordering a build enforced already put a command after the command that produced a file +it discovered, then putup shall not treat that command as having raced that discovery. + +### REQ-IMPL-MEMBERSHIP + +- leg: invariant +- conformance: unclassified +- reference: upstream reaches a discovered reader through its dependency graph the way it reaches a declared one, so it has no content comparison to gate that reader on; no line citation read yet (#277) +- discharge: test "Scenario: A discovered consumer re-runs for a producer that rewrites the same bytes" + +When a command runs and another command was recorded as having read a file that command +produces, putup shall schedule the reader for that build and shall not schedule it again on a +later build in which the producer did not run. + +### REQ-IMPL-REAPPEAR + +- leg: invariant +- conformance: unclassified +- reference: upstream mechanism not read; putup's per-build absence mark has no upstream counterpart that has been read, and the merge copying an out-of-scope entry over this build's own is putup's own scoping machinery (#237) +- discharge: test "Scenario: A recreated dependency does not carry its deletion mark forward" + +If a recorded dependency a previous build routed as absent exists again, then putup shall +record it as present rather than carrying the earlier absence forward. + +### REQ-IMPL-SCANSIBLING + +- leg: invariant +- conformance: putup-only +- reference: putup's dependency scan is a second command derived from the compile, a putup construct whose upstream counterpart is the access capture in tup's server (`src/tup/server/`, not read); the scan declares no graph output, so nothing reaches it through the output cascade. This is the scheduling half of REQ-IMPL-SURVIVE, which states the recording half (#228) +- discharge: test "Scenario: Transitive implicit-dep header tracking" + +When a command runs, putup shall also run the dependency scan derived from that command, so +that a header the command newly reaches through an already-tracked header is recorded. + --- ## Group: output-set @@ -528,6 +581,26 @@ classification the previous record gave that file. Where a command sits in an inactive conditional branch, putup shall not record its declared outputs as generated files. +### REQ-OUT-BRANCHOFF + +- leg: invariant +- conformance: unclassified +- reference: REQ-KEY-RETIRE records tup v0.8-8-g4247a523 deleting the output of a rule turned off by its `ifdef`, so the observable matches; upstream's ownership lives in its database rather than in a record the build carries forward (`tup_db_set_type` in db.c, cited by REQ-OUT-OWNERSHIP), and that path was not read for this case (#369) +- discharge: test "Scenario: Turning a branch off keeps ownership of what it built" + +When a conditional branch that produced an output becomes inactive, putup shall keep the +attribution the previous record gave that output and delete the output as stale. + +### REQ-OUT-PHI + +- leg: invariant +- conformance: putup-only +- reference: upstream never evaluates an unsatisfied branch, so a path declared by several branches never arises (`src/tup/parser.c` skips the block, as REQ-OUT-INACTIVE records); the phi model that registers both branches is putup's own +- discharge: test "Scenario: An output declared by both conditional branches stays tracked" + +If an output declared by more than one branch of a conditional is missing from disk, then putup +shall schedule the command that produces it rather than reporting the build up to date. + --- ## Group: group-membership @@ -564,6 +637,16 @@ from group membership. When a command stops contributing an output to a group, putup shall schedule the commands that consume that group. +### REQ-GRP-GUARDED + +- leg: invariant +- conformance: unclassified +- reference: bounds REQ-GRP-ROUTE, which is putup's own deviation; upstream schedules no consumer for any group-member removal (measured there on tup v0.8-8-g4247a523), so the two agree on this case for unrelated reasons and no upstream mechanism was read +- discharge: test "Scenario: Removing a group member schedules nothing when the group's only consumer is guarded off" + +When a command stops contributing an output to a group whose only consumer sits in an inactive +conditional branch, putup shall schedule no command. + --- ## Group: env-values @@ -609,3 +692,33 @@ it. If a configuration value changes and no command read it, then putup shall leave every command unscheduled. + +### REQ-ENV-SUBPROCESS + +- leg: invariant +- conformance: tup-conformant +- reference: tup stores each environment variable as a node whose value is the `VAR=value` string (`envdb_set` in db.c), compares that stored value against `getenv` on every update and marks the node modified on a mismatch (`env_cb`, reached from `tup_db_check_env`), and builds the command's subprocess environment from its sticky environment entries (`tup_db_get_environ`, called from `update` in updater.c), so the value rather than the rendered text is what re-runs the command +- discharge: test "Scenario: Exported env var consumed via subprocess environment triggers rebuild" + +When a command reads an exported variable through its inherited environment rather than through +its rendered text, putup shall fold that variable's value into the command's identity. + +### REQ-ENV-CONDITION + +- leg: invariant +- conformance: unclassified +- reference: upstream marks a changed environment node modified (`env_cb`, reached from `tup_db_check_env` in db.c) and processes those nodes ahead of parsing (`process_config_nodes` in updater.c), but whether a condition reading the variable registers it as a dependency of the Tupfile was not read +- discharge: test "Scenario: Env var change in a conditional rebuilds the affected branch" + +When an environment variable a Tupfile's condition reads changes value, putup shall schedule +the commands the newly taken branch declares rather than reporting the build up to date. + +### REQ-ENV-IMPORTED + +- leg: invariant +- conformance: deliberate-deviation +- reference: upstream re-reads the process environment on every update and treats an absent variable as a changed value - `env_cb` (db.c) counts `getenv` returning NULL against a stored value as a mismatch and stores NULL - so tup re-runs the commands that read it; putup keeps the value its `import` recorded, so a build is reproducible from the record rather than from whichever shell ran the first one +- discharge: test "Scenario: Imported env vars persist across builds" + +While a variable a previous build imported is absent from the environment, putup shall use the +value it recorded for that variable rather than an empty one. diff --git a/spec/requirements/configure.ears.md b/spec/requirements/configure.ears.md new file mode 100644 index 00000000..013d04bb --- /dev/null +++ b/spec/requirements/configure.ears.md @@ -0,0 +1,118 @@ +# Configure — requirements + +- area: configure +- required-legs: none + +Two subjects meet here. The first is which `tup.config` a directory's Tupfile is evaluated +against: putup collects every `tup.config` from the output root down to that directory and +merges them, the parent overriding the child, so a composed subtree can ship defaults an +integrator overrides from above (`docs/reference.md` §6.1). The second is the `configure` +command, which runs only the rules that produce `tup.config` files, so that a build can be +given configuration a rule generated rather than configuration a user wrote. + +The two meet at a chicken-and-egg problem: the directories `configure` generates configuration +for have none while it runs. `configure` therefore parses against the build root's +`tup.config` alone, and its own products are read by the per-directory merge on the build that +follows. + +This area requires no legs. Neither subject carries state across builds: the merge is a +function of the configuration files present, and `configure` produces files rather than a +record. The configuration a build reads persists because nothing rewrites it, which is the +subject of one requirement here rather than a record leg. + +Upstream tup has no counterpart to the `configure` command — its subcommand dispatch in +`main.c` has no such command, `init_command` is what creates `.tup`, and `updater()` reads a +configuration the user wrote — so the requirements about the command are `putup-only`. The +configuration files themselves do have an upstream counterpart, and the per-directory merge is +a deviation from it. + +See `README.md` for the format and the rules that apply to every area. + +--- + +## Group: inheritance + +Which configuration a directory's Tupfile is evaluated against when the directory has none of +its own. + +### REQ-CONFIGURE-INHERIT + +- conformance: deliberate-deviation +- reference: upstream gives a variant one configuration file — `updater` reads the variant's `tup.config` entry once through `tup_db_read_vars`, and `variant_add` registers a variant by that single file — so there is no ancestor to inherit from and no merge; putup reads a `tup.config` per directory instead, so a subtree composed into a larger project can carry its own configuration (`docs/reference.md` §6.1) +- discharge: test "Scenario: Subdir inherits from parent when no local config" +- discharge: test "Scenario: Root config used when no intermediate configs" + +Where a directory has no `tup.config` of its own, putup shall evaluate its Tupfile against the +configuration of the nearest ancestor directory that has one, rather than against an empty +configuration or against the directory's own file alone. + +### REQ-CONFIGURE-INHERIT-EMPTY + +- conformance: deliberate-deviation +- reference: upstream has one configuration file per variant (`tup_db_read_vars`, `variant_add`), so an empty one is the whole configuration there and there is nothing for it to shadow; under putup's per-directory merge (`docs/reference.md` §6.1) it must shadow nothing +- discharge: test "Scenario: Empty subdir config does not block parent merge" + +Where a directory's `tup.config` sets no variable, putup shall still evaluate its Tupfile +against the ancestors' configuration, rather than reading the file's presence as the whole +configuration for that directory. + +--- + +## Group: scope + +What `configure` reads, runs, and creates. + +### REQ-CONFIGURE-ROOT-ONLY + +- conformance: putup-only +- discharge: test "Scenario: Configure uses root tup.config only" + +While running `configure`, putup shall resolve a config-generating rule's `@()` references from +the build root's `tup.config`, so that a directory whose `tup.config` this run is about to +generate parses before that file exists. + +### REQ-CONFIGURE-NO-RECORD + +- conformance: putup-only +- discharge: test "Scenario: Configure does not create .pup directory" + +While running `configure`, putup shall create no build record directory, rather than +initialising a project that has not been built yet. + +### REQ-CONFIGURE-DEPS + +- conformance: putup-only +- discharge: test "Scenario: Configure handles config rule depending on non-config rule" + +When a rule that produces configuration consumes a file another rule produces, putup shall run +that other rule under `configure` as well, rather than scheduling the configuration-producing +commands alone and leaving the rule's input unbuilt. + +--- + +## Group: reporting + +What `configure` may claim it produced. + +### REQ-CONFIGURE-CREATED + +- conformance: putup-only +- discharge: test "Scenario: A configure that cannot write tup.config does not report creating it" + +If `configure` cannot write `tup.config`, then putup shall fail and name that file in its +error, rather than announcing the file as created before the write has succeeded. + +--- + +## Group: persistence + +What a build may do to the configuration `configure` gave it. + +### REQ-CONFIGURE-PERSIST + +- conformance: putup-only +- discharge: test "Scenario: Config selection persists across multiple builds" + +When a build runs after `configure`, putup shall leave the values in `tup.config` as +`configure` left them, rather than re-deriving the configuration each build and giving a build +with nothing to do something to write. diff --git a/spec/requirements/dep-scan.ears.md b/spec/requirements/dep-scan.ears.md index 1a095316..58319f2e 100644 --- a/spec/requirements/dep-scan.ears.md +++ b/spec/requirements/dep-scan.ears.md @@ -51,6 +51,7 @@ Which commands a scan is generated for. - discharge: test "matches_gcc_compile refuses a command whose first invocation is not a compile" - discharge: test "A command whose first invocation is not a compile is scanned nowhere" - discharge: test "ClangClScanner scans the prefix before an invocation that is not a compile" +- discharge: test "Scenario: The object of a compile that runs elsewhere is reported instead of scanned wrongly" Where a command runs an invocation that is neither a compile putup recognizes nor one it proves inert, whether a loop, a directory change, a standalone environment assignment, a link or any @@ -95,6 +96,17 @@ such invocation, each carrying that invocation's own flags and its own source-fi each one preprocesses a different translation unit and the object it writes is covered only by a scan derived from it. +### REQ-SCAN-CARRIES-COMPILE-WORDS + +- conformance: putup-only +- discharge: test "Scenario: A header the compile reads only under -O2 is tracked" +- discharge: test "Scenario: Implicit deps survive a flag whose path is a separate word" + +Where putup builds a scan from a compile's invocation, putup shall carry the compile's flags into +the scan, a flag whose path stands as a word of its own together with that word, rather than +preprocessing the translation unit under a reduced flag set that resolves the other arm of an +include the compile gated on a flag such as `-O2`. + ## Group: reporting What putup says about an object it did not scan. The unit is the object, not the rule: per object @@ -111,8 +123,36 @@ speaks about the object it names, not about the command that declares it. - discharge: test "Scenario: A depfile flag the compile never carried hides no unscanned object" - discharge: test "A depfile flag outside the scannable prefix suppresses nothing" - discharge: test "A depfile flag inside the scannable prefix still suppresses" +- discharge: test "Scenario: The object of a compile that runs elsewhere is reported instead of scanned wrongly" When a rule declares an object file that no generated scan covers — every object it declares, where no scan at all is generated — and no invocation a scan would have been built from carries a depfile flag, putup shall name that object and the rule's Tupfile under `parse`, and report how many such objects exist under a build. + +## Group: scan-results + +What putup does with what a scan printed. A scan is a command putup wrote itself, so its output is +a contract rather than a report: putup knows the rule it asked for, and anything else the driver +printed is not a dependency. The one dependency that looks unusable is the one outside the source +tree; it is recorded, not dropped, and #305 was filed on the assumption of the opposite. + +### REQ-SCAN-REJECTS-FOREIGN-OUTPUT + +- conformance: putup-only +- discharge: test "Scenario: A dep scan that prints anything but its rule fails the build" + +If a scan writes anything ahead of the make rule it was generated to produce, then putup shall +fail the build and name the scan's command rather than record what it read, because a note line +taken for a dependency path names a file that does not exist, and a dependency that never stats +leaves the compile it feeds out of date at every build from then on. + +### REQ-SCAN-OUTSIDE-TREE-ABSOLUTE + +- conformance: putup-only +- discharge: test "Scenario: A dependency outside the source tree is recorded rather than dropped" + +Where a dependency a scan reports resolves outside the source tree, putup shall record it under +its absolute path rather than skip it, because a header under a sysroot or a toolchain prefix is +one the compile read like any other, and dropping it — the arm a relativize-or-skip reading of the +path would take — leaves the compile silently stale after a toolchain change. diff --git a/spec/requirements/group-references.ears.md b/spec/requirements/group-references.ears.md new file mode 100644 index 00000000..2af0bbff --- /dev/null +++ b/spec/requirements/group-references.ears.md @@ -0,0 +1,50 @@ +# Group references — requirements + +- area: group-references +- required-legs: none + +A group is declared by the rules that name it as an output and referenced by the rules that +consume it. The subject here is the reference: which directory's group a written reference +resolves to, what has to happen before it can be resolved, and which edges the resolution puts +into the graph. How a resolved reference is spelled inside a command is `operand-flags`; what +the record keeps of the resulting edges is `command-record`. See `README.md` for the format and +the rules that apply to every area. + +This area requires no legs. A reference resolves during the parse and is a function of the +project's Tupfiles and the trees the build was pointed at, not of state carried from the +previous build. + +--- + +## Group: resolution + +Which group a written reference names, and what must be parsed before that can be answered. + +### REQ-GROUPREF-DEMAND-PARSE + +- conformance: deliberate-deviation +- reference: upstream needs no rule of this shape because a group reference resolves against a database rather than against this build's parse — tup's input path list creates the group itself when the lookup misses (`tup_db_create_node_part` with `TUP_NODE_GROUP`, the bracketed-name branch of `parser.c`'s input path handling), and `add_input` then carries that node as an input whether or not the producing directory has been parsed in this run; the link is resolved by the updater afterwards. putup keeps no node database between the parse and the graph, so a reference to an unparsed directory's group has nothing to resolve against: it parses that directory on demand instead of inventing a placeholder a later parse would have to reconcile +- discharge: test "Scenario: Bang macro order-only groups trigger demand-driven parsing" + +When a rule expands a `!`-macro whose order-only inputs name a group in a directory this build has +not parsed yet, putup shall parse that directory before resolving the reference rather than +resolving it to an empty group. + +### REQ-GROUPREF-PRODUCING-DIRECTORY + +- conformance: putup-only +- discharge: test "Scenario: Cross-directory groups in 3-tree builds" + +Where a group reference is spelled through variables that expand to a path across the separate +source, config and build trees, putup shall resolve it to the group declared by the directory +that expanded path names rather than to the directory the referencing Tupfile sits in. + +### REQ-GROUPREF-INPUTS-SECTION + +- conformance: tup-conformant +- reference: upstream recognises a bracketed name in a rule's inputs section, not only after the order-only separator: the same input path-list branch that creates a missing group (`tup_db_create_node_part` with `TUP_NODE_GROUP` in `parser.c`) runs on the normal input list, and `add_input`'s `TUP_NODE_GROUP` arm adds that node to the rule's input set, so the reference names the group rather than a file. `operand-flags`' REQ-OPERAND-GROUP-IS-AN-OPERAND records the same list carrying it as an operand, measured against tup +- discharge: test "Scenario: Group references in regular inputs expand correctly" + +When a rule names a group in its inputs section rather than after the order-only separator, +putup shall resolve the bracketed name to that group and expand `%` to the group's +members rather than read the name as a file to be found on disk. diff --git a/spec/requirements/record-read.ears.md b/spec/requirements/record-read.ears.md index e4c3c7e9..8ab166c4 100644 --- a/spec/requirements/record-read.ears.md +++ b/spec/requirements/record-read.ears.md @@ -3,8 +3,8 @@ - area: record-read - required-legs: none -The subject here is what the reader does when a record's declared positions do not hold up, or when -what it reads at them repeats or contradicts itself. Every offset in the index is a number the +The subject here is what the reader does when a record's declared positions do not hold up, when its +bytes do not match the checksum it carries, or when what it reads at them repeats or contradicts itself. Every offset in the index is a number the record itself supplies, so a reader validates before it trusts; a value in range can still be wrong, and the one form of wrongness a reader can see unaided is a claim the record's other claims deny. The question this area settles is what happens at the moment either check fails. It is a @@ -97,6 +97,14 @@ as the field's value rather than treating it as a failed read. Where a record's operand data fails validation, putup shall still recover the paths its file table records, because that read examines the file table alone. +### REQ-READ-REJECT-FOREIGN-CONTENT + +- conformance: putup-only +- discharge: test "Scenario: A build record that is not putup's own is refused out loud" + +If a record's contents do not match the checksum it carries, then putup shall report the record as +unreadable rather than reading the sections whose declared positions validated. + ## Group: announcement What the build says about a record it could not load. diff --git a/spec/requirements/record-write.ears.md b/spec/requirements/record-write.ears.md new file mode 100644 index 00000000..c4d20490 --- /dev/null +++ b/spec/requirements/record-write.ears.md @@ -0,0 +1,59 @@ +# Writing a record — requirements + +- area: record-write +- required-legs: none + +The subject here is what the record a build writes says, as opposed to what a later build makes of +it. Two properties of the written bytes are settled in this area: a path appears in exactly one +entry, and the content is a function of the project rather than of the run that produced it — of +which trees the build ran in, in which order its jobs finished, and whether it had anything to do. +These are properties of one write, not obligations of a category of state carried between builds, +so this area declares no legs. See `README.md` for the format and the rules that apply to every +area, and `record-read.ears.md` for what a reader does with a record that breaks them. + +Both properties exist because the record is read back by something other than the build that wrote +it. A second entry for one path is a fork in the reader's lookup — one entry carries the recorded +state a later build compares against and the other does not — which is why the reader treats a +record naming one path twice as unreadable (`REQ-READ-REJECT-SELF-CONTRADICTION`); the writer's job +is not to produce one. Content that varies run to run has no such reader-side failure: it simply +makes every comparison of two records, and every claim that a build changed nothing, unfalsifiable. + +Upstream tup keeps its state in a SQLite database rather than a serialized record, so the +byte-content properties here have no upstream counterpart and are `putup-only`; the uniqueness +property does, because a database schema is where upstream states it. + +--- + +## Group: uniqueness + +How many entries one path gets. + +### REQ-WRITE-ONE-ENTRY-PER-PATH + +- conformance: tup-conformant +- reference: upstream tup's `node` table declares `unique(dir, name)` in db.c's schema, so a directory-and-name pair names at most one node and a second insert for it is rejected by the database rather than by the walk +- discharge: test "Scenario: A build records one entry per path" + +When a build writes its record, putup shall write exactly one entry per path rather than one entry +per directory chain that reached that path. + +## Group: determinism + +What the record's content is allowed to depend on. + +### REQ-WRITE-SAME-TREE-SAME-SHAPE + +- conformance: putup-only +- discharge: test "Scenario: Two builds of one tree record the same thing" + +When the same project is built twice in trees that share nothing, putup shall write records of the +same shape and size rather than recording edges in the order the jobs that produced them finished. + +### REQ-WRITE-NOOP-SAYS-THE-SAME + +- conformance: putup-only +- discharge: test "Scenario: Two builds of one tree record the same thing" + +While a build has nothing to run, putup shall write a record whose entries, edges, operands and +strings say what the record it read said, rather than one that restates them as the run it did not +need to do would have. diff --git a/spec/requirements/reset.ears.md b/spec/requirements/reset.ears.md index f56236b7..ad7f0ae8 100644 --- a/spec/requirements/reset.ears.md +++ b/spec/requirements/reset.ears.md @@ -55,3 +55,17 @@ build was in tree. If a reset cannot read the build record, then putup shall keep that record and name the command that resets the project without it, rather than removing a record whose files it could not name. + +## Group: removal-reporting + +What a reset may claim it removed. + +### REQ-RESET-COUNT-REMOVED + +- conformance: putup-only +- discharge: test "Scenario: clean does not count an empty directory it could not remove" + +If a reset cannot remove a directory its removal pass emptied, then putup shall report the +platform's own failure message and leave that directory out of both the removed count and the +removal announcements, rather than counting the attempt or restating the path that message +already names. diff --git a/spec/requirements/scoped-builds.ears.md b/spec/requirements/scoped-builds.ears.md new file mode 100644 index 00000000..4f721020 --- /dev/null +++ b/spec/requirements/scoped-builds.ears.md @@ -0,0 +1,81 @@ +# Scoped builds — requirements + +- area: scoped-builds +- required-legs: none + +The subject here is the scope filter: what a build restricted to one or more directory arguments +may look at, and what it may say when it fails. A scope narrows the work a build performs; it is +not a category of state carried across builds, so this area declares no legs. The state the +requirements below lean on — the recorded content of a file, the recorded text of a command — is +owned by `command-record` and `record-read`, and the sentences here only forbid the scope filter +from being applied to it. + +That is the whole of this area's content: a scope is a restriction on *work*, never on +*observation*. A build that narrows what it looks at cannot know whether the part it skipped +invalidates the part it built, and the failure is silent — the build reports success, or reports +nothing to do, with a stale artifact on disk. Upstream tup draws the same line: its target +arguments reach the graph through `prune_graph`, after the DAG has already been built from a +whole-tree modify list, and `mark_nodes` walks incoming edges, so everything upstream of a target +survives the pruning whatever edge reaches it. + +The reporting group covers the other direction — what a scoped build may claim when it fails. A +remedy is only worth printing where it can change the outcome, and `-a` changes the outcome only +for a build that left rules out of scope. + +See `README.md` for the format and the rules that apply to every area. + +--- + +## Group: detection + +What a scoped build must still see outside its scope. + +### REQ-SCOPE-DECLARED-INPUT + +- conformance: tup-conformant +- reference: `prune_graph` and `mark_nodes` — target arguments prune the DAG after it is built from a whole-tree modify list, and marking walks incoming edges, so a file upstream of a target survives pruning whatever kind of edge reaches it +- discharge: test "Scenario: A scoped build sees a declared input outside the scope change" + +While a build is restricted to a scope, putup shall detect a change to a file that a rule in that +scope declares as an input even where the file lies outside the scope, whatever kind of edge +carries the dependency, rather than exempting out-of-scope files from the comparison except for +the edge kinds some bypass list happens to name. + +### REQ-SCOPE-OUT-OF-SCOPE-HEADER + +- conformance: tup-conformant +- reference: `prune_graph` and `mark_nodes` — a header reached only by a discovered include is still upstream of the command that read it, so it survives the same pruning a declared input does +- discharge: test "Scenario: Scoped build detects header changes outside scope" +- discharge: test "Scenario: Scoped build with multiple scopes detects out-of-scope header changes" +- discharge: test "Scenario: Out-of-tree variant build picks up out-of-scope header changes" +- discharge: test "Scenario: Scoped rebuild after a SCOPED initial multi-scope build" + +While a build is restricted to one or more scopes, putup shall re-run every in-scope command that +read a header changed outside those scopes and then re-run the in-scope commands consuming those +commands' outputs, whether the record sits in the source tree or under a build directory and +whether the preceding build was full or itself scoped, so that neither a report of nothing to do +nor a link against the objects the edit did not reach can stand. + +### REQ-SCOPE-COMMAND-TEXT + +- conformance: unclassified +- reference: upstream's comparison of a command's rendered text runs in its parser phase, which has not been read; `prune_graph` establishes only that the target arguments are applied after the DAG is built +- discharge: test "Scenario: Tupfile changes detected regardless of scope" + +While a build is restricted to a scope, putup shall treat a command whose rendered text differs +from the text recorded for it as changed even where the Tupfile that renders it lies outside that +scope, rather than comparing recorded identities only for the commands the scope filter admits. + +## Group: reporting + +What a scoped build may claim when an input resolves to nothing. + +### REQ-SCOPE-GHOST-HINT + +- conformance: putup-only +- discharge: test "Scenario: The ghost hint offers -a only where -a could help" + +When a build fails on an input that no rule it parsed produces, putup shall offer `-a` only where +that build was restricted to targets and was not already given `-a`, and otherwise name the +removal of the producing rule as the likely cause, rather than printing one hint that sends a +build the remedy cannot change round the same failure. diff --git a/spec/requirements/tupfile-evaluation.ears.md b/spec/requirements/tupfile-evaluation.ears.md new file mode 100644 index 00000000..faf478f5 --- /dev/null +++ b/spec/requirements/tupfile-evaluation.ears.md @@ -0,0 +1,54 @@ +# Tupfile evaluation — requirements + +- area: tupfile-evaluation +- required-legs: none + +The subject here is what a Tupfile's text means as it is evaluated: which included files +contribute, and what an assignment operator does to a variable that already has a value. It +stops where a rule is emitted — the rule's own operands, its globs and its outputs each have +their own area. + +This area requires no legs. Evaluation carries no state across builds: the same text, the same +configuration and the same tree evaluate to the same result, so its requirements are invariants +of that function and carry no `leg` field. + +See `README.md` for the format and the rules that apply to every area. + +--- + +## Group: includes + +What a repeated `include` of one file contributes. + +An `include` is deduplicated so that a file reached twice along the same path does not emit its +rules twice. The unit of that deduplication is the file *together with the conditional context +it sits in*, because a conditionally-guarded include and an unguarded one are two different +contributions of the same text: the guarded copy's rules carry the guard, the unguarded copy's +do not, and skipping the second because the first was seen drops everything the second would +have added. + +### REQ-EVAL-INCLUDE-CONTEXT + +- conformance: deliberate-deviation +- reference: upstream `parser_include_file` holds no include-once set at all and reparses the named file on every `include`, so upstream never skips a repeat; putup deduplicates repeats instead, and this requirement bounds that deduplication to a single conditional context rather than lifting it to the file +- discharge: test "Scenario: Include first seen in a dead branch still applies when included actively" +- discharge: test "Scenario: Include first seen in a config-inactive branch is reprocessed when included actively" + +Where a Tupfile includes a file that was first included under a conditional context other than +the one this include sits in, putup shall process that file again rather than skip it as already +included. + +--- + +## Group: assignment + +What an assignment operator does to a variable that is already defined. + +### REQ-EVAL-SOFT-ASSIGN + +- conformance: putup-only +- reference: upstream `set_variable` recognises `+=`, `:=` and plain `=` only, and tup.1's variable section documents no `?=`, so there is no upstream counterpart to defer to +- discharge: test "Scenario: ?= soft assignment - = takes precedence" + +Where a Tupfile assigns a variable with `?=` and that variable is already defined, putup shall +keep the defined value rather than replace it with the assignment's own value. diff --git a/spec/requirements/variant-builds.ears.md b/spec/requirements/variant-builds.ears.md new file mode 100644 index 00000000..c6da7560 --- /dev/null +++ b/spec/requirements/variant-builds.ears.md @@ -0,0 +1,83 @@ +# Variant builds — requirements + +- area: variant-builds +- required-legs: none + +The subject here is a build whose outputs land somewhere other than beside the sources: a variant +build under `-B`, and the three-tree build where the source root, the configuration root and the +build root are three separate places. One question runs through all of it — given a path written in +a Tupfile, which node does it name. The same file is reachable by several spellings at once: a +rule declares `include/header.h` and a consumer in another directory references +`$(B)/include/header.h`, or `../data.txt`, or a `../` chain whose length depends on which +subdirectory did the spelling. Every one of those has to arrive at the node the producing rule +created, because a second node at a second spelling is a dependency nothing ever satisfies and a +rebuild that never converges. + +Resolution is a function of the Tupfiles and the three roots, so this area declares no legs. Several +of its requirements are witnessed by a no-op rebuild, but what those scenarios probe is still +identity: the record and the graph must spell one file one way, or the comparison across builds is +comparing two files. The obligations of recorded state itself are `command-record`'s, not this +area's. + +Upstream tup has variants and solves the same identity problem, by mirroring the source tree into +the variant directory as database entries that carry a `srcid` back to their source entry. It does +not have this area's other two shapes: a build root outside the tup hierarchy (`variant_add` names +a variant directory by its entry under the tup root), and a configuration tree separate from the +source tree. Requirements about those are `putup-only`. + +See `README.md` for the format and the rules that apply to every area. + +--- + +## Group: node-identity + +Which node a path spelled in a Tupfile names, when more than one spelling reaches the same file. + +### REQ-VARIANT-OUTPUT-NODE + +- conformance: tup-conformant +- reference: upstream parses each Tupfile against the variant's own directory entry (`parse` sets `tf.variant` from `tup_entry_variant` before `parse_tupfile` runs, `src/tup/parser.c`), so a rule's outputs are created under the variant directory rather than at the source spelling, and a variant entry reaches its source counterpart through `srcid` (`variant_get_srctent`, `variant_tent_to_srctent`, `src/tup/variant.c`) instead of a second entry; putup carries the same rule with paths grounded at the build root rather than with mirrored database entries +- discharge: test "Scenario: Variant outputs are automatically mapped to build directory" +- discharge: test "Scenario: Order-only deps on generated outputs resolve correctly in variants" + +Where a build writes outside the source tree, putup shall represent a rule's output and every +reference to it, spelled source-relative or build-relative, as one node under the build root. + +### REQ-VARIANT-UNRESOLVED-UPGRADE + +- conformance: tup-conformant +- reference: upstream turns an existing entry into a real file by changing its type in place, keeping its tupid and so every link already recorded against it — `ghost_to_file` (`src/tup/create_name_file.c`), and `tup_db_set_type` on the found entry in `gitignore` (`src/tup/parser.c`), which the comment there names as the variant-to-in-tree case; neither path deletes the entry and inserts a replacement +- discharge: test "Scenario: Cross-directory regular inputs work in variant builds" + +When a directory parsed before its producer references a file a later-parsed directory generates, +putup shall keep the edges recorded against the unresolved reference by changing that node in place +once it becomes the generated file, rather than by creating a second node for the producer. + +### REQ-VARIANT-BUILD-ROOT-DEPTH + +- conformance: putup-only +- discharge: test "Scenario: Cross-project order-only dependency resolution" + +Where a reference to the build root is written from a subdirectory so that it carries fewer parent +elements than the build root's own name, putup shall resolve it to the generated file the producing +rule declared rather than by comparing the two chains of parent elements as text. + +### REQ-VARIANT-RECORD-SPELLING + +- conformance: putup-only +- discharge: test "Scenario: Sibling directory inputs work with incremental variant builds" + +putup shall derive a cross-directory input's spelling in the build record with the derivation the +build graph uses, rather than with a second derivation that renders a parent reference differently. + +## Group: roots + +Which of the three roots a path is resolved against. + +### REQ-VARIANT-GLOB-ROOT + +- conformance: putup-only +- discharge: test "Scenario: Config tree inside source tree" + +Where the configuration root lies inside the source root, putup shall expand a rule's globs against +the source root rather than against the configuration root.