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
40 changes: 40 additions & 0 deletions spec/requirements/change-detection.ears.md
Original file line number Diff line number Diff line change
@@ -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.
113 changes: 113 additions & 0 deletions spec/requirements/command-record.ears.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
118 changes: 118 additions & 0 deletions spec/requirements/configure.ears.md
Original file line number Diff line number Diff line change
@@ -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.
40 changes: 40 additions & 0 deletions spec/requirements/dep-scan.ears.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Loading
Loading