Series contents · Engineering and Delivery · 阅读中文版
A colleague wants to verify a two-line change. They run the build script, and compilation fails halfway through. No disaster: use the previously installed tool to investigate. Except the installation directory is now empty.
“I pressed Build. Why did it take away the old tool?”
That confusion usually means a script has combined several responsibilities: load the environment, synchronize dependencies, configure the project, compile, clear the installation prefix, install, and trim the results. Each step may be reasonable on its own. Together, they make it difficult to answer: “At this failure point, which artifacts are still trustworthy?”
The reviewed changes separated environment preparation, building, and installation, so compiling no longer also cleared installed artifacts. This was more than adding scripts. It redefined failure boundaries.
Three directories, three kinds of ownership
The source tree holds inputs. The build tree holds results derived from those inputs. The installation tree holds interfaces consumed downstream. They may share a workspace, but they should not share a lifecycle.
1 | Source files and explicit dependencies |
Suppose configuration generates a Python helper directly into the installation directory. Merely configuring the project has now changed state visible to downstream users. A later installation cleanup may delete that freshly generated file. Generating it in the build tree and copying it through installation rules makes the dependency graph more honest: generation produces a build artifact; installation consumes it.
Likewise, automatically updating an external repository makes “build” mean both network synchronization and compilation. The source may be unchanged while dependencies move. A network outage can prevent a compilation that could otherwise finish offline. Making dependency preparation an explicit prerequisite reduces this uncertainty. The principle is separation of responsibilities, not a mandate for one directory layout.
Why an environment script needs a version
A new shell does not know the current project’s installation prefix, dependency locations, or Python search paths. An environment script establishes those conventions, but the shell may outlive a particular revision of the script.
That creates a subtle problem: the script on disk has changed, while the terminal still holds yesterday’s exported variables. Checking that variables are nonempty is insufficient. A nonempty value may belong to another workspace or an obsolete convention.
A simple approach assigns a revision marker to the environment contract. Build and installation entry points check that marker and the required variables. A mismatch produces an early request to reload the environment. A path inconsistency that would otherwise appear halfway through a build becomes a missing prerequisite detected before work begins.
The marker is not a complete environment fingerprint. Toolchain files can be replaced even when the marker is correct, and a user can override a variable manually. Stronger reproducibility requires tool versions, dependency revisions, and configuration arguments. One revision string does not pin every input.
What happens when you load the environment repeatedly?
The common implementation prepends a directory to PATH every time. Load it ten times and ten copies line up. Switch workspaces and old and new entries become interleaved.
A more careful strategy checks complete path entries before adding them. A substring check is insufficient: /opt/compiler and /opt/compiler-old are different directories.
Workspace isolation is another constraint. If the build and installation trees still point at a previous project, updating only the source root can mix two versions. Environment preparation therefore needs to check how related paths belong to the current workspace and provide a clear override mechanism.
The reviewed scripts added checks around these relationships. In general, however, a string-prefix test is not proof of filesystem containment after normalization. Cleanup also needs to account for symbolic links, parent-directory traversal, and special cases such as the filesystem root. Those are further design requirements, not evidence that every historical version handled them completely.
Why installation deserves its own stage
An explicit installation entry point can answer four questions in one place:
- Has the build directory been configured?
- Is the destination a directory this project is allowed to replace?
- Which files must exist after installation?
- How can downstream users identify the build that produced it?
The last question is easy to overlook. An executable in a directory need not come from the current source. An installation receipt can record the source revision, build type, dependency prefixes, and key artifacts. It is not a complete supply-chain proof, but it gives “Which version am I using?” a checkable starting point.
The receipt should be written after installation checks finish, or explicitly distinguish “installing” from “ready.” A crash immediately after an early receipt could cause a partial installation to be treated as complete. The reviewed script installed files, checked required paths, then wrote the receipt. That supports recording stage completion; it does not establish an atomic directory replacement.
A failure matrix is worth more than ten smooth builds
The following proposed tests help examine the design.
| Failure point | Desired observable state |
|---|---|
| Environment not loaded | No artifact changes; explain what is missing |
| Dependency prefix absent | Fail before configuration and identify the dependency |
| Compilation fails | Preserve diagnostics and intermediates; do not imply installation succeeded |
| Required module missing after installation | Do not declare the delivery complete |
| Installation prefix points at a shared directory | Refuse cleanup |
| Old shell remains after environment script changes | Detect a contract-revision mismatch |
| Workspace changes | Relevant paths refer to one consistent workspace |
Paths with spaces, unset variables, different build configurations, and explicit custom dependencies also deserve coverage. Script defaults are part of the interface. Whether Debug or Release is the default should not be guessed from documentation. When defaults change historically, code and documentation must be compared from the same period.
Does separation make everyday work harder?
An extra command does add operational cost. A higher-level wrapper can still combine stages, provided it clearly states which stages it runs. A combined entry point for routine use and individual entry points for debugging serve different needs.
Performance claims require restraint. Avoiding installation-tree cleanup on every build can reduce repeated work. Explicit dependencies may make incremental builds more predictable. Neither observation justifies a percentage speedup without measurement. Measure configuration, compilation, installation, and packaging separately, rather than crediting cache warmth or network variation to script improvements.
The immediately explainable benefit is clearer state. A compilation failure no longer automatically implies an installation failure, and installation success is more than the script’s last line happening to print a cheerful message.
When the next colleague asks, “Can I rebuild while keeping the tool I currently use?”, the system should have an unambiguous answer.
Series contents · Previous · Next
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !