Your platform team must decide what a compile-time expansion may read beyond the source it is given, so where do you draw the line?
answer
- the stage is code on the build machine
- undeclared inputs bake into the artifact
- same source, different output
- caches cannot see what they were not told
- pin the input or move it to a step
basics
~20 sDraw it at declared, version-pinned inputs checked into the repository. Anything else an expanding stage reads is baked into the artifact as a literal, so identical source stops producing identical output and build caches can no longer tell what is stale.
solid answer
~40 sThe expanding stage is ordinary code running on the build machine, so it can usually reach the clock, the environment, the filesystem and the network. Every such read is silently folded into the artifact as a constant. The rule worth setting is that an expansion is a **function of its declared inputs**: the source it is handed plus anything checked in and version-pinned. The reasons are reproducibility (identical source, identical artifact), caching (a toolchain cannot invalidate on an input nobody declared), auditability (a value in the artifact with no origin in the source), and supply chain (a dependency's stage runs with your builder's access). The cost is real: teams lose a convenient shortcut and must move genuine outside reads into an explicit generation step whose output is reviewed and committed.
go deeper
Understand that building a project runs code from the project's own tooling, not just your source, so a build is not a passive read of files.
Explain the concrete consequence: whatever the stage reads while expanding becomes a constant in the artifact, and the source no longer shows where that value came from.
Argue it from operations. Undeclared inputs defeat build caches and incremental rebuilds, and they produce the failure that reproduces on one machine and not another.
This is the trade you own. Purity buys reproducible, cacheable, auditable builds and costs teams an explicit generation step. Decide the rule, the escape hatch, and who reviews the exceptions.
## What the stage can reach A compile-time expansion is not a template engine reading a file. In most toolchains it is arbitrary code executing inside the build process, which means it can do whatever that process can do: read the clock, read environment variables, read files outside the module, open a socket, generate a random number. Some toolchains deliberately restrict the stage to an effect-free subset; others impose nothing. Knowing which kind you are in is the first question, because the second question -- what should it be allowed to do -- only has teeth if the answer is enforceable. The mechanical fact underneath the whole decision is simple: **whatever the stage reads is rendered into the artifact as a constant**, and the source then shows the call, not the value. ## Four things an undeclared read costs 1. **Reproducibility.** Two builds of identical source produce different artifacts. Every downstream comparison -- is this binary the one we reviewed, did this change actually change anything -- stops working. 2. **Caching and incremental builds.** A build system invalidates on inputs it was told about. An expansion that reads something undeclared produces stale output that nothing knows is stale, which surfaces as the worst class of bug: it works on one machine and fails on another, and a clean build fixes it until it does not. 3. **Auditability.** The artifact carries a value with no origin anywhere in the repository. A reader six months later can see the constant and cannot see where it came from, because the code that fetched it ran once, elsewhere, and left nothing behind. 4. **Supply chain.** Allowing a dependency's expansions means running that dependency's code on your build machine, with your builder's filesystem access, environment and credentials. This is a trust decision of the same weight as running its installation scripts, and it is usually made implicitly. ## The rule worth setting A defensible default, stated as a rule other teams can follow: - An expansion is a **pure function of its declared inputs**: the source fragment it receives, plus files that are checked in, version-pinned and declared to the build system. - **No clock, no environment, no network, no randomness** inside an expanding stage. - A genuine need for outside data goes to an **explicit generation step** whose output is written to disk, reviewed like source, and committed. - Exceptions exist but are **named, reviewed and few**, with the exceptional input declared to the build system so caching stays honest. ## What the rule costs | Situation | Under the rule | What the team gives up | |---|---|---| | Expansion derived from a checked-in description | Allowed as-is | Nothing | | Expansion needs data from a live service | Moved to a generation step | Convenience; gains a reviewable diff | | Expansion wants a build timestamp or identifier | Passed in as a declared input | Direct reads; gains cache correctness | | A dependency ships its own expansions | Reviewed and pinned | Speed of adoption | The honest cost is friction. A generation step is a pipeline someone maintains, its output is a diff someone reviews, and the loop from "the upstream description changed" to "the code changed" gets one step longer. Teams that skip the rule are not being reckless for no reason -- they are buying that step back. The judgment is whether reproducible, cacheable, auditable builds are worth a pipeline, and at more than a handful of engineers the answer is usually yes. ## Enforcing it - **Declare inputs explicitly** so the build system can invalidate correctly, and treat an undeclared input as a defect rather than a style preference. - **Run the same build twice in different environments and on different days**, then compare artifacts. A difference points straight at something a stage read that nobody declared. This is the cheapest detector there is. - **Sandbox where the toolchain allows it**, restricting what the stage may reach rather than relying on convention. - **Review what an expansion emits**, not only how it is called. A stage reading a value nobody declared looks perfectly ordinary at every call site. ## Where the line genuinely moves The rule is not universal. A single-maintainer tool with a five-second build and no external consumers pays a lot of process for benefits it will not feel. A platform whose artifacts are deployed by other teams, cached across a fleet and attested for provenance cannot function without it. State the rule at the level where the build is actually shared, and write down the escape hatch alongside it -- an unwritten exception path is what turns a good rule into one everyone quietly routes around.
- What is the escape hatch when an expansion genuinely needs data from outside?Move the read out of the compile and into an explicit generation step whose output is written down, reviewed and committed. The expansion then reads only checked-in source. You trade a convenient shortcut for a diff somebody can inspect and a build that repeats.
- How do you find out whether an expansion already has an undeclared input?Build the same source twice, in different environments and at different times, then compare the artifacts. A difference points at something a stage read that nobody declared. Repeating on a clean machine catches the common cases: clock, environment and locally cached files.
- Why is this a trust decision rather than only a build-hygiene one?Because allowing expansions from a dependency means executing that dependency's code inside your build, with whatever access the build environment has. The output is also less visible than ordinary code, since it is generated during the compile rather than reviewed in a diff.
saying these in an interview costs you the question
- Says reading the build clock is harmless because it is only a timestamp
- Assumes the build cache notices an input it was never told about
- Treats expansion as data processing rather than code running on the build machine
- Believes reproducible builds are only about avoiding random numbers
- Thinks banning every outside read costs the team nothing