epm — API reference
2/30 types documented. Of 234 public methods, 96 carry their own description, 0 are covered by their type's, and 138 have neither. An entry with no prose below its signature is undocumented in the source, not undocumented here.
CLASS BuildLedger
The set of modules already compiled during ONE epm build invocation.
This exists so epm build --all can treat its workspace member list as an
unordered SET. Data declares mda and Regex as dependencies and all three
are members, so without a ledger the run compiles mda twice — once as Data's
dependency and once in its own right. With it, whichever reaches the module
first builds it and the rest skip.
It is a class rather than a field on Main because Main.start() implements
TaskStarter, whose start() is declared non-MODIFY — so the whole dispatch
chain down to the build is non-MODIFY and cannot write a field on Main.
Passed as MUTABLE REFERENCE BuildLedger, a non-MODIFY caller can still call
mark through it (the borrow ABI makes that a plain BuildLedger*).
Skipping is a COMPILE skip only — never a trust skip. The Layer B consent
check runs over the full graph of every target before any of it is built, so a
dependency that was already compiled is still checked against epm.lock for
whoever depends on it next.
epm/src/BuildLedger.ev:25
Constructors
INIT()
Methods
METHOD has(String name) RETURNS boolean
TRUE if a module of this manifest name has already been built here.
MODIFY METHOD mark(String name) RETURNS void
Record a module as built. Idempotent.
METHOD count() RETURNS int64
How many distinct modules this invocation compiled — the one honest summary line for a workspace build.
CLASS BuildOutcome
IMPLEMENTS Cloneable
epm/src/BuildOutcome.ev:16
Constructors
INIT()
INIT(int64 index, String name, int32 code, String out, String err, String message)
Methods
METHOD clone() RETURNS Cloneable
METHOD at() RETURNS int64
METHOD moduleName() RETURNS String
METHOD exitCode() RETURNS int32
METHOD stdoutText() RETURNS String
METHOD stderrText() RETURNS String
METHOD note() RETURNS String
CLASS BuildPlan
epm/src/BuildPlan.ev:20
Constructors
INIT()
Methods
METHOD isLoaded() RETURNS boolean
METHOD name() RETURNS String
METHOD targetKind() RETURNS String
METHOD outputPath() RETURNS String
The final artifact path, as evc published it. Never reconstructed from the
module name plus a platform library shape — that is flag construction, and
getting it wrong is silent (a bare -o libfoo.dylib lands in the cwd).
METHOD unitCount() RETURNS int64
MODIFY METHOD load(String outDir) RETURNS boolean
Read <outDir>/module_plan.json. FALSE when absent or unreadable — the
caller falls back to the compiler's own path, which is still the live one.
MODIFY METHOD loadFile(String planPath) RETURNS boolean
Read a plan from an explicit path.
--single publishes <stem>.plan.json beside the source rather than one
module_plan.json per directory, because several .ev files can share a
directory in single mode and a per-directory name would have them
overwrite each other. The SCHEMA is identical, so everything below this
point is unchanged.
METHOD diagnosticIndex(String code) RETURNS int64
Index of code in the carried diagnostics, or -1. An older plan carries
none, so every lookup misses and the caller falls back to the bare tool
output — degrading rather than inventing a diagnostic it does not have.
METHOD diagCategory(int64 i) RETURNS String
METHOD diagMessage(int64 i) RETURNS String
METHOD diagExample(int64 i) RETURNS String
METHOD diagCorrected(int64 i) RETURNS String
METHOD diagSee(int64 i) RETURNS String
METHOD manifestCompilerFlags() RETURNS String[]
Flags a MANIFEST declared — the module's own plus those merged from its
dependencies. Profile and toolchain flags are deliberately absent: those
are policy and come from build-config.json. Data links -lpcre2-8 only
because its dependency Regex declared it.
METHOD manifestLinkerFlags() RETURNS String[]
METHOD stagePairCount() RETURNS int64
How many artifacts --install would copy into the toolchain's lib/.
METHOD stageSourceAt(int64 i) RETURNS | String
METHOD stageDestAt(int64 i) RETURNS | String
METHOD sourceCount() RETURNS int64
The .ev files this module was emitted from. Published so a build system
can ask whether the emit is current without re-deriving the source set
from a manifest it would have to parse a second time.
METHOD sourceAt(int64 i) RETURNS | String
METHOD includeFlags() RETURNS String[]
-I<dir> for each published include DIRECTORY. The plan deliberately
carries paths, not flags; this is where a path becomes a flag.
METHOD linkArgs() RETURNS String[]
The full link argument list, in order: kernel, then dependencies, then
foreign. Order is semantic to ld, so it is preserved as published.
METHOD unitAt(int64 i) RETURNS | BuildUnit
CLASS BuildUnit
IMPLEMENTS Cloneable
epm/src/BuildUnit.ev:12
Constructors
INIT(String s, String l, String o)
Methods
METHOD clone() RETURNS Cloneable
METHOD sourcePath() RETURNS String
METHOD language() RETURNS String
METHOD objectPath() RETURNS String
CLASS Builder
epm/src/Builder.ev:8
Fields
String compilerSettingString selfPathString lastCompilerOutString lastCompilerErrString lastMessageboolean forceBuildboolean noInlineboolean quiet
Constructors
INIT(String compilerSetting, String selfPath)
Methods
MODIFY METHOD setForce(boolean on) RETURNS void
MODIFY METHOD setNoInline(boolean on) RETURNS void
MODIFY METHOD setQuiet(boolean on) RETURNS void
--quiet, forwarded rather than assumed.
epm used to pass --quiet to the compiler on every invocation. That is a
decision about what the DEVELOPER sees, and epm was making it for them:
--quiet suppresses info and warning output, so every warning the
compiler raised was discarded before anyone could read it — including in
run_smokes.sh, which builds the whole smoke corpus through epm. Errors
were never affected, which is why this went unnoticed: the builds that
failed still said why, and the ones that merely warned said nothing.
Now it is off by default and passed through only when asked for.
MODIFY METHOD setExternalFlags(REFERENCE REFERENCE String[] compilerFlags, REFERENCE REFERENCE String[] linkerFlags) RETURNS void
The compiler and linker flags contributed by this target's external
dependencies, resolved ONCE by Main and handed down.
Builder does not resolve them itself for two reasons. It has no view of
the dependency graph — Main.buildTarget owns that — and resolving per
module would re-run pkg-config and the installer query for every module
in a graph, where the design says once per build.
The set is the UNION over the whole target graph, so a dependency may
receive a flag only its sibling needed. That is benign and is already how
manifest buildFlags behave, and narrowing it per module would mean
carrying a separate graph per dependency for no gain anyone can observe.
METHOD findCompilerScript(String moduleFolder, String selfPath) RETURNS String
Locate the compiler, or return "" — never a guess.
The ladder, in order:
1. EV_COMPILER — an explicit override always wins.
2. Beside epm itself, then bin/evc walking up from epm's own
directory. This is the rule the Makefile states: epm is installed
into bin/ next to the compiler so one directory on $PATH gives
both. It holds wherever the toolchain lives and does not care where
the user's project is.
3. The module being built — for an in-repo module when epm was invoked
by a bare name carrying no directory.
4. PATH.
It previously fell back to the BARE STRING "evc" and handed that to
python3 unchecked, which resolved against the caller's cwd. In this repo
bin/ is not on PATH, so every build died with a raw interpreter error
naming a path nobody wrote:
python3: can't open file '/Users/…/Envzn/evc': No such file or directory
Returning "" instead lets the caller say what actually went wrong.
MODIFY METHOD buildViaPlan(String moduleFolder, boolean install, boolean optimize, boolean release, boolean force, String selfPath) RETURNS int32
Build a module through the PLAN: evc --emit-only for the front half,
then compile and link from module_plan.json + evbuild.
Phase 7: this is the DEFAULT path, so the compiler's own compile/link
half now has no callers left. EV_EPM_NATIVE_BUILD=0 still selects the
old delegating invokeCompiler, which hands the whole build to evc —
kept as the oracle this path is diffed against, and as the rollback.
MODIFY METHOD buildSingleFile(String evPath, boolean prod, String selfPath) RETURNS int32
Build ONE .ev FILE — evc --single, then this toolchain.
The compiler emits the C++ and publishes a plan; everything after that is
the same path a module takes. Single mode used to do its own compile and
link, composing a clang line from the compiler's own _CXX,
_BASE_CXXFLAGS and kernel paths — so it was the one entry point a build
system could not drive, and the one place a flag could differ from every
other build without anything noticing.
It also linked with no -o, which is why a stray a.out appears in
whatever directory the compiler ran from. The plan names the output.
When --single emits .ll instead of .cpp, nothing here changes: the
unit's lang becomes "ll" and compilePrefix already has that arm.
MODIFY METHOD compileAndLink(REFERENCE REFERENCE BuildPlan plan, boolean install, boolean optimize, boolean force, String selfPath) RETURNS int32
Compile and link one loaded plan. The BACK HALF, shared.
A module and a --single file differ only in how their plan is produced:
evc <folder> --emit-only versus evc --single <file> --emit-only, and
where the plan lands. From here down they are the same object — units,
include paths, link arguments, an output — which is the point of the plan
being a description rather than a procedure. Sharing this is not a tidy-up:
two copies would be two places for the flag order to drift, and the whole
invariant is that argv is composed once.
optimize, not prod: this half never emits, so posture cannot reach it.
It selects the toolchain profile — -O3 -march=native versus -g — and
nothing else.
MODIFY METHOD runProcess(String[] cmd) RETURNS boolean
Run one composed step. The argv LIST goes straight to Process->run, never a joined string, so no quoting rule is load-bearing.
MODIFY METHOD invokeCompiler(String moduleFolder, boolean install, boolean optimize, boolean release) RETURNS int32
METHOD getStr(JsonValue obj, String field) RETURNS String
MODIFY METHOD buildDev(String moduleFolder, boolean stage, boolean optimize, boolean release) RETURNS int32
stage asks the compiler to ALSO install this module's artifacts into
lib/ (--install), which a library must do if anything is to link it
later. Callers pass TRUE for a shared library and FALSE for an executable.
It is a parameter rather than always-on because it is only correct for a
library: epm build in a user's application should not scatter that
application into the toolchain's lib/.
This is what the deleted per-module Makefile targets did — every one of
them passed --install — and omitting it made epm build --all look
like it worked while the modules downstream quietly linked whatever an
earlier make bootstrap had staged. A workspace build on a clean lib/
is the case that exposes it, and that is precisely the case --all exists
to serve.
METHOD kernelDylibPath() RETURNS String
Where the kernel's libENVZN sits, or "" if it cannot be found.
make install stages it into
MODIFY METHOD buildDist(String moduleFolder, String[] depDylibs) RETURNS int32
Package a RELOCATABLE artifact.
The tarball used to hold the binary and the manifest and nothing else, while the binary's rpaths were absolute paths into the build machine's source tree:
path /Users/brian/Developer/Envzn/kernel
path /Users/brian/Developer/Envzn/evTest/build
So it could not run anywhere but the machine that produced it — the dylibs were missing AND the search paths pointed at someone else's disk. Fixing one without the other fixes nothing, which is why they land together.
The layout is a directory, not a bare binary, because the loader needs somewhere to look:
<name>-<version>/
<name> the executable
<name>.json the manifest
lib/ libENVZN + every dependency dylib
@loader_path/lib is ADDED rather than replacing the absolute entries.
dyld tries rpaths in order, so on the build machine the absolute ones
still resolve and on any other machine they simply miss and fall through
to the bundled directory. Deleting them would mean parsing otool -l
output to learn what to delete; the cost of leaving them is that the
artifact reveals the build tree's layout, which is worth recording but
not worth that machinery here.
macOS only for now: install_name_tool is Darwin's. Linux is a v1
target and wants $ORIGIN/lib via patchelf — same shape, different
tool, and the step is skipped rather than faked when the tool is absent.
CLASS Config
epm/src/Config.ev:7
Fields
String installLocationString cacheDirString compiler
Constructors
INIT()
Methods
MODIFY METHOD read(String filePath) RETURNS void
METHOD get(String key) RETURNS String
The value for key, or "" when the file did not carry it.
Used for external.<package>.prefix and its .include / .libdir
siblings — rung 2 of the resolution ladder, and the place a PATH belongs,
since a module author cannot know the layout of a machine they have never
seen.
METHOD write(String filePath) RETURNS void
CLASS Externals
epm/src/Externals.ev:29
Constructors
INIT()
Methods
METHOD mergePlatform(REFERENCE REFERENCE JsonValue entry, String platform) RETURNS JsonValue
Flatten one entry's platforms block over its base fields.
The merge is FIELD BY FIELD, not whole-object replacement, so a package
that differs between platforms only in its library name states only that.
The spelling is taken from Go's #cgo darwin LDFLAGS: directives, where
the conditional modifies the field rather than duplicating the
declaration.
Implemented by building a new object rather than by reading the entry
twice: copy every base field the platform block does not override, then
append the platform's own. readEntry then has a single flat object and
one field-reading pass, instead of the same IF-chain written twice.
METHOD readEntry(REFERENCE REFERENCE JsonValue flat) RETURNS ExternalDep
Read one already-flattened entry into a struct.
include and libdir are stored as written. Whether one is absolute is
a question asked at the point of use, not a field carried around.
MODIFY METHOD parse(REFERENCE REFERENCE JsonValue root, String platform, MUTABLE REFERENCE MUTABLE REFERENCE ExternalDep[] out) RETURNS void
Read a manifest's whole external-dependencies array.
platform is passed in rather than detected here: this class does no I/O
in Phase 8.1, which keeps it testable against a fixture without a
machine probe. 8.2 adds the detection alongside the registry read.
A refused entry is SKIPPED and the walk continues, so a manifest with one bad declaration reports that one rather than going dark on the rest.
MODIFY METHOD validate(ExternalDep d, int64 index) RETURNS boolean
Whether d is a usable declaration. Refusals set .lastError;
portability problems only add a note.
The character class on a package name is not cosmetic. The name becomes part of an environment-variable name at resolution time, so a separator or an expansion in it is an injection route (design §12.4).
METHOD validName(String n) RETURNS boolean
^[a-z][a-z0-9-]{0,63}$, checked without a character loop.
find_first_not_of SUCCEEDS at the first character outside the set, so a
FAILURE means every character is allowed — the pipe-XOR reads backwards
from the name, which is worth the comment.
MODIFY METHOD note(String msg) RETURNS void
Append a non-fatal problem for Main to print.
MODIFY METHOD refuse(String msg) RETURNS void
Record a refusal. Keeps the first one in lastError.
METHOD readStrings(REFERENCE REFERENCE JsonValue arr, MUTABLE REFERENCE MUTABLE REFERENCE String[] out) RETURNS void
Read a JSON array of strings.
A third copy of a helper that BuildPlan.ev and Toolchain.ev each keep
privately. Promoting it is a separate cleanup; duplicating four lines is
cheaper than a refactor that touches two working classes.
METHOD findBuildConfig(String selfPath) RETURNS String
Locate epm/build-config.json by walking up from the running binary.
Mirrors Toolchain.findEvbuild, which finds epm/tools/evbuild.py the
same way, and for the same reason: epm may be invoked from anywhere and
the registry lives beside its own source, not beside the caller.
MODIFY METHOD loadRegistry(String configPath) RETURNS void
Read the registry and the host platform from build-config.json.
A missing or unreadable file is NOT fatal: a package whose registry entry
is absent can still resolve through the environment, the machine config or
the standard directories, and a framework or system link needs no
registry at all. Silence here becomes a located message later, from
missingMessage, which is the right place for it.
METHOD registryFor(String name) RETURNS NativeDep
The registry entry for name, or an empty one.
METHOD resolvePrefix(ExternalDep d, REFERENCE REFERENCE Config cfg, String moduleFolder) RETURNS Resolution
Where d lives on this machine, and at what version.
Five rungs, first hit wins, every candidate checked by verifyPrefix
before it is accepted. Discovery runs only for a library link whose form
is not source: a framework, a system library and a vendored source tree
have nothing to find.
The INSTALLER rung comes BEFORE pkg-config, deliberately. pkg-config answers with a version-exact path that a routine upgrade invalidates, while the installer answers with a symlink that survives one. The version is then read from pkg-config SEPARATELY, whichever rung supplied the prefix, because it is the only rung that knows one.
No rung carries a timeout: Process.run has none, so a wedged
pkg-config wedges the build exactly as a wedged clang already does.
METHOD verifyPrefix(ExternalDep d, String prefix) RETURNS boolean
Whether prefix actually holds what d asked for.
The HEADER is checked first and that ordering is load-bearing: a machine carrying a library's runtime package but not its development headers otherwise passes here and then dies inside clang, which is the failure this design exists to replace with a sentence.
The library match is a PREFIX match on lib<name>. rather than an exact
filename, because a real install is versioned — libssl.3.dylib,
libpcre2-8.0.dylib. It looks in <libdir> and one directory below it,
which is what accepts Debian's multiarch layout. There is no glob engine
in the kernel or in epm, so this is listFiles plus prefix and suffix
compares, the idiom Manifest already uses.
/usr and /usr/local are the documented escape: a header present there
means the development package is installed, and therefore so is the
library, wherever the distribution chose to put it.
METHOD dirFor(String prefix, String rel, String fallback) RETURNS String
The directory to search: rel when a manifest gave an ABSOLUTE one, else
prefix/rel, else prefix/fallback.
An absolute value is used AS GIVEN and never appended to a prefix. Brian's
decision of 2026-09-12 is that whatever a manifest hands us is what we
use; joining it to a discovered prefix produced paths like
/opt/homebrew/opt/pcre2//opt/homebrew/include, which verified against
nothing and made an honest declaration unbuildable. validate still warns
that such a manifest will not build on another machine.
METHOD libraryUnder(String dir, String libName, String form) RETURNS boolean
Whether dir, or any directory one level below it, holds lib<name>.*
with the extension form implies.
METHOD extensionFits(String fileName, String form) RETURNS boolean
Whether a filename's extension matches the declared form.
form is the whole reason this is a field: -lfoo finds either a shared
object or an archive depending on search order, so intent has to be
stated rather than inferred.
METHOD versionFromPkgConfig(String pkgConfigName) RETURNS String
A package's version according to pkg-config, or "".
The ONLY rung that knows a version, which is why a version constraint is advisory: four of the five external dependencies in this repo yield nothing here.
METHOD versionFromHeader(String headerPath, String macro) RETURNS String
The version macro out of a vendored header, for form: source.
A vendored tree has no package manager to ask, so the header is the only
witness. Reads #define <macro> "x.y.z" and returns what is between the
quotes.
METHOD envNameOf(String name) RETURNS String
The package name as an environment-variable fragment: uppercased, with
- mapped to _.
Safe because validate already refused anything outside
[a-z][a-z0-9-]*, so no separator or expansion can reach here.
There is no String.replace in the kernel, so the dash mapping is a
split and a rejoin.
METHOD toolOnPath(String tool) RETURNS boolean
Whether tool is executable somewhere on PATH.
METHOD versionWarning(ExternalDep d, Resolution r) RETURNS String
A warning when the resolved version does not satisfy the constraint, or "" when it does, when there is no constraint, or when either side cannot be parsed.
WARNS, permanently, and never fails — Brian's decision of 2026-09-12. A constraint is documentation plus a diagnostic. Four of the five external dependencies in this repo yield no version at all, so a constraint that cannot be checked is the normal case rather than the exception, and an unparseable version is treated as absent rather than as a problem.
Reuses Semver, which already owns range parsing and comparison for
package versions. A second comparator would be a second place for the
same bug.
METHOD missingMessage(ExternalDep d) RETURNS String
What to say when a declared package could not be found.
Names the package, the constraint, every rung that was tried, and the install command for THIS platform. This is the message that replaces a clang header-not-found, so it has to carry everything a developer needs to act without reading the design document.
Installer names are PRINTED and never executed (design §12.3): epm does not install system libraries, on the evidence that Conan and vcpkg both do and neither displaced the platform package manager.
METHOD joinHeaders(REFERENCE REFERENCE String[] headers) RETURNS String
a, b for a header list, or the package's own name when it has none.
METHOD reportLine(Resolution r) RETURNS String
One line for the resolution report, printed before evc is invoked.
Four columns: package, version, location, how it was found. A dash where a version is genuinely unknowable, which is most of them. This is the whole of the design's disclosure position — the developer is the detector for a header-version mismatch, so the facts have to be in front of them on every build.
METHOD compilerFlags(ExternalDep d, Resolution r, MUTABLE REFERENCE MUTABLE REFERENCE String[] out) RETURNS void
The compiler flags one resolved external contributes.
An include flag only when the package declares headers, because a system
link has nothing to include. Defines travel WITH the external rather than
sitting in the module's own buildFlags, which is what lets a second
module linking the same package inherit them instead of copying them.
METHOD linkerFlags(ExternalDep d, Resolution r, MUTABLE REFERENCE MUTABLE REFERENCE String[] out) RETURNS void
The linker flags one resolved external contributes.
A framework is a TWO-TOKEN pair and must stay one, which the existing
flag de-dup already understands. A system library is a bare -l. A
source package contributes nothing: its objects come from the existing
nativeSources path and link like any other unit.
A static archive is passed as its resolved ABSOLUTE PATH, not as -L
plus -l, because the darwin linker prefers a sibling .dylib on the
same search path and would silently link the shared copy instead. This is
the conclusion Cargo reached when it grew an explicit static link kind.
NO runtime search path is emitted. -Wl, is a denied flag pattern whose
own rationale says rpath is driver-owned, and pkg-config hands back a
version-exact prefix that a routine upgrade turns into a dangling entry.
A prebuilt library already records its own install name, so the emission
bought nothing.
METHOD archivePathFor(String dir, String libName) RETURNS String
The full path of lib<name>.a under dir or one level below, or "".
METHOD isPublic(ExternalDep d, REFERENCE REFERENCE String[] foreignHeaders) RETURNS boolean
Whether this external's headers reach a CONSUMER of the declaring module.
DERIVED, never declared. A module's emitted .hpp carries whatever its
manifest lists under foreign.headers, spliced in unconditionally with no
inlining or profile guard. So an external is public exactly when its own
declared headers intersect that list, and no author can get it wrong by
writing the answer down twice and disagreeing with themselves.
On this repo's corpus that makes OpenSSL public to Networking's consumers and Accelerate public to AdvancedMath's, while Regex's PCRE2 is private — Regex reaches it through a C++ shim rather than through a bound header, so nothing leaks.
METHOD readForeignHeaders(REFERENCE REFERENCE JsonValue root, MUTABLE REFERENCE MUTABLE REFERENCE String[] out) RETURNS void
A manifest's foreign.headers list.
MODIFY METHOD agree(REFERENCE REFERENCE ExternalDep[] all, REFERENCE REFERENCE String[] owners) RETURNS boolean
Whether every module declaring the same package agrees about it.
owners[i] is the module that declared all[i]. Several modules MAY
declare one package — Cargo makes native-library ownership exclusive and
that rule would reject the Networking-plus-Crypto graph this repo is
heading for. What they may not do is disagree.
link and form must match, because the two produce incompatible command
lines. And a package absorbed as static or source by more than one
module in one graph is refused outright: both forms copy the library into
a separate artifact, so the process would end up holding two copies of its
global state, which is a correctness bug rather than wasted bytes. That is
the one place Cargo's exclusivity instinct is right, and it is right
precisely because the form is absorbing.
METHOD absorbing(String form) RETURNS boolean
Whether a form copies the library into the consuming artifact.
METHOD unionLibraries(REFERENCE REFERENCE String[] a, REFERENCE REFERENCE String[] b, MUTABLE REFERENCE MUTABLE REFERENCE String[] out) RETURNS void
The union of two declarers' library lists, ordered by the LONGER list.
Order is a link-time input, not a set: ssl depends on crypto, so a
naive first-seen union that met a crypto-only declarer first would invert
them and break a static link. Taking the order from the longer list keeps
the declarer that knows the whole package authoritative about its
sequence.
METHOD listHas(REFERENCE REFERENCE String[] items, String want) RETURNS boolean
MODIFY METHOD collectFromGraph(String rootFolder, REFERENCE REFERENCE ResolvedDep[] nodes, MUTABLE REFERENCE MUTABLE REFERENCE ExternalDep[] out, MUTABLE REFERENCE MUTABLE REFERENCE String[] owners) RETURNS void
Every external declared anywhere in one resolved graph, with the module that declared each.
The root module is collected SEPARATELY because Resolver.graphNodes
holds only its transitive dependencies and not the module being built.
Missing that is how a module's own declaration would have gone unread.
The design assigned this to Resolver. It lives here instead, because the
ruling is that ONE class owns external dependencies and Resolver's job
is resolving Envzn packages; a graph walk it already performs is cheaper
to read from than to re-enter.
MODIFY METHOD collectFrom(String moduleFolder, MUTABLE REFERENCE MUTABLE REFERENCE ExternalDep[] out, MUTABLE REFERENCE MUTABLE REFERENCE String[] owners) RETURNS void
Append one module folder's declarations, tagged with its own name.
METHOD anyDeclared(REFERENCE REFERENCE ExternalDep[] all) RETURNS boolean
Whether any module in the graph declared an external at all.
Lets a build stay exactly as quiet as it is today until something is actually declared — the report is disclosure, not decoration.
METHOD linkedLibraries(String artifactPath, MUTABLE REFERENCE MUTABLE REFERENCE String[] out) RETURNS void
The absolute library paths a built artifact records, read out of the artifact itself.
The ARTIFACT is the source of truth here, not a record epm keeps. There is no lock file in this design, so there is nothing to go stale and nothing that can disagree with reality — the binary says what it will load, and this reads it with the platform's own tool.
@rpath and @loader_path entries are skipped: those are Envzn's own
modules, resolved relative to the binary, and they are not external
dependencies. What remains is the absolute install names a prebuilt
library dictated, which is the thing that can be missing on another
machine and the thing this design accepts as given rather than rewrites.
METHOD missingPaths(REFERENCE REFERENCE String[] paths, MUTABLE REFERENCE MUTABLE REFERENCE String[] gone) RETURNS void
Which of paths no longer exist on disk.
OS-OWNED PREFIXES ARE SKIPPED, and that is not a shortcut. Since macOS 11
the system libraries live in the dyld shared cache and have NO file on
disk at all, so /usr/lib/libSystem.B.dylib is both perfectly loadable
and perfectly absent. Testing it reported three false failures on the
first run of this check. What matters here is the libraries a package
manager installed, which are real files and really can move.
METHOD osOwned(String path) RETURNS boolean
Whether a path belongs to the operating system rather than to a package manager, and therefore need not exist as a file.
CLASS Fetcher
epm/src/Fetcher.ev:9
Fields
String httpsLitString httpLitString gitLitString ghLit
Constructors
INIT()
Methods
METHOD splitGitHubUrl(String gitUrl) RETURNS GitHubRepo
METHOD downloadTarball(String gitUrl, String tag, String cacheDir) RETURNS | String
METHOD listTags(String gitUrl) RETURNS | Tag[]
METHOD extractTarball(String tarballPath, String destDir) RETURNS STATUS
METHOD resolveSha(String gitUrl, String tag) RETURNS | String
METHOD resolveVersion(String gitUrl, String range) RETURNS | Tag
CLASS JsonParser
epm/src/JsonParser.ev:7
Fields
String srcint32 pos
Constructors
INIT()
Methods
MODIFY METHOD parse(String source) RETURNS JsonValue
METHOD peek() RETURNS char32
MODIFY METHOD advance() RETURNS void
MODIFY METHOD skipWhitespace() RETURNS void
MODIFY METHOD parseValue() RETURNS JsonValue
MODIFY METHOD parseObject() RETURNS JsonValue
MODIFY METHOD parseArray() RETURNS JsonValue
MODIFY METHOD parseString() RETURNS JsonValue
MODIFY METHOD parseNumber() RETURNS JsonValue
MODIFY METHOD parseBool() RETURNS JsonValue
MODIFY METHOD parseNull() RETURNS JsonValue
METHOD emit(REFERENCE REFERENCE JsonValue v) RETURNS String
METHOD emitValue(REFERENCE REFERENCE JsonValue v) RETURNS String
METHOD emitObject(REFERENCE REFERENCE JsonValue v) RETURNS String
METHOD emitArray(REFERENCE REFERENCE JsonValue v) RETURNS String
METHOD emitString(String s) RETURNS String
CLASS JsonValue
IMPLEMENTS Cloneable
epm/src/JsonValue.ev:7
Constructors
INIT(String kind)
Methods
METHOD clone() RETURNS Cloneable
MODIFY METHOD setString(String v) RETURNS void
MODIFY METHOD setInt(int64 v) RETURNS void
MODIFY METHOD setBool(boolean v) RETURNS void
MODIFY METHOD addField(String name, REFERENCE REFERENCE JsonValue value) RETURNS void
MODIFY METHOD addItem(REFERENCE REFERENCE JsonValue value) RETURNS void
METHOD isObject() RETURNS boolean
METHOD isArray() RETURNS boolean
METHOD isString() RETURNS boolean
METHOD isNumber() RETURNS boolean
METHOD isBool() RETURNS boolean
METHOD isNull() RETURNS boolean
METHOD asString() RETURNS String
METHOD asInt() RETURNS int64
METHOD asBool() RETURNS boolean
METHOD hasField(String name) RETURNS boolean
CLASS LockFile
epm/src/LockFile.ev:7
Constructors
INIT()
Methods
MODIFY METHOD read(String filePath) RETURNS void
METHOD write(String filePath) RETURNS void
MODIFY METHOD addEntry(LockEntry e) RETURNS void
METHOD findByName(String name) RETURNS LockEntry
CLASS Main
IMPLEMENTS TaskStarter
epm/src/Main.ev:7
Constructors
INIT(String[] argv)
Methods
METHOD runBuild(String[] argv) RETURNS int32
METHOD isProdProfile(String variant) RETURNS boolean
Build every member of <wsName>-workspace.json, found by walking up from
the current directory.
Members are a SET, not a sequence: each member's dependency graph is
resolved and every module is built at most once, tracked by name in
.builtNames. So Data listing mda and Regex as dependencies costs
nothing extra when those are also members — whichever comes first wins and
the rest are skipped. The file therefore never encodes build order, which
is the accumulation problem the per-module make targets had.
Does this variant compile with the PROD profile?
dist implies it — a package you hand someone is optimised or it is not
worth handing over — and --prod asks for it directly. Everything else is
dev. One predicate rather than four equals("dist") tests, because the
question "is this optimised" was previously answered in four places and
would have needed a fifth every time a caller was added.
METHOD isReleasePosture(String variant) RETURNS boolean
Does this variant emit in RELEASE posture?
A SECOND question, and the one that is easy to miss. Optimisation is a
compile-line fact; posture changes what is EMITTED — non-always_on ASSERT
is stripped and WHEN Build IS RELEASE becomes the live arm. -prod
answers yes to both, which is right for something you ship and wrong for
something you test: a corpus should run optimised code AND still exercise
the assertions it exists to exercise.
optimized is exactly that gap. It is what the smoke suite had by
accident before the profile became explicit, and it is now askable.
METHOD buildWorkspace(String wsName, String variant, boolean verbose, boolean force, boolean noInline, boolean quiet, int64 jobs, String selfPath) RETURNS int32
METHOD buildWorkspaceParallel(REFERENCE REFERENCE Workspace ws, String wsName, String variant, boolean verbose, boolean noInline, boolean quiet, boolean force, int64 jobs, String selfPath) RETURNS int32
Build the workspace in DEPENDENCY WAVES, several members at a time.
A wave is every not-yet-built member whose in-workspace dependencies are
all built. mlearn names AdvancedMath and stats, so it cannot appear
until both have; Json, mda, Regex and money name nothing and all
appear in wave 1. The edges are read from each member's own manifest —
the dependency tree is stated, never inferred from the member ORDER in the
workspace file, which is just a list and carries no ordering promise.
Only in-workspace edges gate a wave. A dependency OUTSIDE the workspace is
not something this run builds, so waiting on it would wait forever; it is
resolved from lib/ exactly as the serial path resolves it.
Progress is guaranteed by construction: if a pass adds nothing while members remain, the remaining edges form a cycle (or name a member that does not exist), and that is reported rather than spun on.
METHOD cfgCacheDir(String rootDir) RETURNS String
The configured package-cache directory for a workspace root.
METHOD newestCompilerSource(String compilerScript) RETURNS int64
The newest mtime among the compiler's own .py sources, or -1 when the
package cannot be located.
METHOD newestPythonIn(String dirPath, int32 depth) RETURNS int64
Newest .py mtime under dirPath, recursively. Depth-bounded for the same
reason the compiler-discovery walk is: a fixed ceiling cannot spin on a
filesystem that reports an odd tree.
METHOD lastBuiltAt(String moduleFolder) RETURNS int64
When this member was last built successfully, or -1 if never.
buildinfo.json is written by the compiler on every successful build, into
the module's artifact dir — build/ for a src/-layout module, the module
root for a flat one. It is the one file that means exactly "a build of this
module finished", which is the timestamp rule 3 needs.
When a build of this module last FINISHED, or -1 if it never has.
The .buildstamp first, because it is the only artifact both build paths
write. buildinfo.json is written by the compiler's full path — after the
link — and never by --emit-only, so on the plan path it does not come
back after a sweep removes it. Reading it alone therefore answered "never
built" forever: every run decided the compiler had moved, swept, rebuilt,
and left the same answer behind for the next one. It kept working only
while the sweep was broken and removed nothing.
buildinfo.json stays as the fallback for a tree built before stamps
existed, and the root is checked after build/ for the flat layout.
METHOD buildSingleTarget(String evPath, String variant, boolean verbose, boolean noInline, boolean quiet, boolean force, String selfPath) RETURNS int32
Build ONE module folder: its dependency graph first, then the module.
ledger records what this invocation has already compiled, so a module
reached twice is built once (see BuildLedger).
A target ending in .ev is ONE FILE, not a module.
epm build path/to/One.ev is the --single shape: no manifest, no
dependencies, no staging. It is dispatched here rather than inside
buildTarget because everything buildTarget does first — find the
manifest, resolve the graph, check the lock file — presumes a module, and
a single file has none of it.
METHOD isSingleFileTarget(String target) RETURNS boolean
METHOD buildTarget(String moduleFolder, String variant, boolean verbose, boolean force, boolean noInline, boolean quiet, String selfPath, MUTABLE REFERENCE MUTABLE REFERENCE BuildLedger ledger) RETURNS int32
METHOD sweepModule(String moduleFolder, boolean regenerable) RETURNS int32
Delete build artifacts directly inside dirPath. Returns the count.
Path->extension() returns the extension WITH its leading dot (".cpp") —
pathSmoke asserts exactly that and Path.ev slices from the dot. This
compared against "cpp" and so matched NOTHING: clean has never removed a
source artifact. Both spellings are accepted, as Manifest already does,
so the code is right whichever belief a reader arrives with.
Sweep a MODULE — both places its artifacts can live.
A library emits into <module>/build/ and an executable emits beside its
sources, so a sweep that looks in one place cleans half the layouts and
reports a confident number for the half it saw. That already cost once:
epm clean Data said "removed 0" while every .cpp, .o and .dylib sat in
build/. The fix went into runClean and NOT into the compiler-changed
sweep, which kept passing the module root alone — so "the compiler
changed, cleaning everything" removed nothing at all, and a build that
then found the module current printed build successful having done
nothing. The message was the only part that worked.
Both callers go through here now, so there is no longer a version of this that can be given one directory. (2026-09-04.)
METHOD runPlan(String[] argv) RETURNS int32
epm plan <module> — the READ SIDE of phase 4.
Loads module_plan.json (what evc emitted and what it needs) and prints
the commands epm WOULD issue. Nothing is executed, nothing is written:
evc's own path is still the live one, so the printed argv can be diffed
against what evc actually runs while both exist.
Toolchain and flag POLICY are NOT in the plan and are not read here. They
come from epm/build-config.json via epm/tools/evbuild.py — the single
definition make and epm share.
METHOD joinArgs(String[] args) RETURNS String
Render an argv list for display. Display only — the real executor will pass the list to Process->run, never a joined string, so no quoting rule is ever load-bearing.
METHOD runStep(String[] cmd) RETURNS boolean
Run one composed step. The argv LIST is passed straight to Process->run — never a joined string — so no quoting rule is ever load-bearing.
METHOD runClean(String[] argv) RETURNS int32
epm clean <module> · epm clean all [--workspace <name>] · epm clean --cache
A SUBJECT IS REQUIRED. It used to default to ".", so a bare epm clean
swept artifacts, the binary and dist/ out of whatever directory you
happened to be standing in — silently, and with no way to tell it was
about to. (2026-09-04.)
all means EVERY MEMBER OF THE WORKSPACE, which is what it reads as. It
used to be a literal alias for --cache, so epm clean all cleaned one
folder plus the package cache — neither of the two things a reader would
expect. The cache is now its own subject.
all is WORKSPACE-SCOPED, never repo-scoped, and never touches the
bootstrap closure: kernel, Networking and epm belong to make clean,
because epm links two of them and cannot build what it links. The two
alls therefore do not overlap.
METHOD cleanOne(String moduleFolder) RETURNS int64
Sweep one module folder: artifacts, its build/ dir, the binary the manifest names, and dist/.
METHOD cleanCache(String anchor) RETURNS int32
The package cache — its OWN subject, no longer implied by all.
METHOD depDeclaredFlags(String depPath) RETURNS String[]
The build flags a resolved dependency declares. Empty when it declares none, or when its manifest cannot be found (which the resolver has already reported).
METHOD extractNameFromGitUrl(String gitUrl) RETURNS String
METHOD installRemote(PackageRef ref, String tag, String cacheDir) RETURNS | String
METHOD manifestMissing(String verb, String folder) RETURNS boolean
Whether folder has no readable module manifest — and if so, say so.
A folder with no manifest used to read as a module with ZERO dependencies,
so epm install bogus printed 0 ok, 0 fetched, 0 failed and exited 0 —
a typo, a wrong relative path and a clean install were indistinguishable
(gh #294). The argument these verbs take is a module FOLDER, not a package
name, so the message says that outright: reaching for epm install Regex
is the natural wrong guess.
No $N: in these format strings — a colon straight after a placeholder is
read as a format spec and swallowed (gh #289).
METHOD runInstall(String[] argv) RETURNS int32
METHOD runAddPath(String[] argv, String name) RETURNS int32
epm add <name> --path <dir> — a LOCAL dependency.
The remote form (<gitUrl>@<version>) was the only one, so a local
dependency could only be added by hand-editing the manifest — and a local
path dep is what every module in this repo actually uses.
The version is READ from the target's own manifest rather than typed by the user: the resolver matches a candidate's concrete version against the requested range, so a hand-typed version that disagrees with the target produces a resolution failure at install rather than an error here.
METHOD runAdd(String[] argv) RETURNS int32
METHOD runUpdate(String[] argv) RETURNS int32
METHOD runInit(String[] argv) RETURNS int32
METHOD runRemove(String[] argv) RETURNS int32
METHOD runList(String[] argv) RETURNS int32
METHOD runTree(String[] argv) RETURNS int32
METHOD printTreeNode(Resolver rs, String name, int32 depth) RETURNS void
METHOD printUsage() RETURNS void
METHOD printHelp() RETURNS void
METHOD describeJson(REFERENCE REFERENCE JsonValue v) RETURNS String
One-line rendering of a JsonValue, deep enough to show that a nested
object actually parsed.
Not a JSON emitter — JsonParser.emit already is one. This exists to be
EYEBALLED against a fixture, so an object prints as {k=v, k=v} rather
than as re-indented JSON you then have to diff by hand.
No $N: in any format string here: a colon straight after a placeholder
is read as a format spec and swallowed (gh #289).
METHOD dumpExternalDeps(String manifestPath, String platform, String selfPath) RETURNS int32
Phase 8.1 gate — read a manifest's external-dependencies through
Externals.parse and print what came back.
platform defaults to darwin but is overridable as a third argument,
which is the only way to exercise the LINUX arm of a platform block on
this machine. The design's platform case is otherwise unverifiable until
somebody runs a Linux build, so being able to prove the MERGE here is
worth one optional argument.
Also prints foreign verbatim, because derived visibility is the
intersection of that list with each entry's headers, and reading them
side by side is how you check a fixture says what you meant.
No $N: in any format string — a colon straight after a placeholder is
read as a format spec and swallowed (gh #289).
METHOD runExternalDeps(String[] argv) RETURNS int32
epm external-dependencies [<folder>] [--verify], short form epm externals.
Without --verify it resolves fresh and prints what each declared
external came to on this machine — the same report a build prints, asked
for on its own. Fresh every time, because there is no lock file: §2.3 of
the design shows a recorded prefix is wrong in one direction or the other,
so recording one would be a false-alarm generator or a no-op.
With --verify it also reads the built artifact and checks that every
absolute library path it records still exists. That is the one thing the
resolution report cannot tell you: a library can move out from under a
binary that was linked correctly, and nothing rebuilds when it does,
because staleness is timestamps over sources. Detection is this command;
remediation is --force.
METHOD externalsEnabled() RETURNS boolean
Whether resolved external flags actually reach the compile and link
lines. ON by default since the Regex migration; EV_EPM_EXTERNALS=0
opts out.
It was off while the mechanism was unproven, which is what let the two
states be compared on one manifest: with buildFlags removed from Regex
and the switch off, clang cannot find pcre2.h; with it on, the build
succeeds. The declaration is load-bearing, so the default flipped.
The opt-out survives as the rollback, the same way EV_EPM_NATIVE_BUILD=0
still selects the pre-plan build path it replaced.
METHOD reportExternals(String moduleFolder, REFERENCE REFERENCE ResolvedDep[] nodes, String selfPath, REFERENCE REFERENCE Config cfg, MUTABLE REFERENCE MUTABLE REFERENCE String[] outCompiler, MUTABLE REFERENCE MUTABLE REFERENCE String[] outLinker) RETURNS int32
Print where every external in this graph resolved, before evc runs.
Prints NOTHING when no module in the graph declares one, so a build is as
quiet as it was until something is actually declared. Unconditional
otherwise: epm's --quiet is forwarded to the compiler and suppresses
none of epm's own output, and disclosure is the point.
Returns non-zero when a declared package is missing or two modules disagree about one, because both are reasons not to start a compile.
METHOD parentFolderOf(String filePath) RETURNS String
The folder holding filePath, or "." when it has no parent component.
A form: source external resolves relative to the MODULE ROOT, so the
fixture's own directory is what the in-tree lookup needs.
METHOD joinList(REFERENCE REFERENCE String[] items) RETURNS String
a, b, c for a printed list, or - when empty.
METHOD dispatch(String[] argv) RETURNS int32
METHOD start() RETURNS STATUS
METHOD runSemverTests() RETURNS int32
METHOD expectSat(Semver sv, String ver, String rng, boolean want) RETURNS boolean
METHOD expectCmp(Semver sv, String a, String b, int32 want) RETURNS boolean
METHOD expectSelect(Semver sv, String[] vers, String rng, String want) RETURNS boolean
CLASS Manifest
epm/src/Manifest.ev:7
Constructors
INIT()
Methods
METHOD findManifestPath(String folder) RETURNS String
MODIFY METHOD read(String filePath) RETURNS void
METHOD write(String filePath) RETURNS void
METHOD writeFiltered(String filePath, String excludeName) RETURNS void
MODIFY METHOD addDependency(PackageRef ref) RETURNS void
METHOD buildFlagList() RETURNS String[]
The module's declared build flags, as ONE list (#34 Layer B).
compiler and linker are concatenated deliberately. The split is
meaningless for trust: the driver splices BOTH lists into the clang++ -c
command line, so a flag hidden under "linker" reaches the compile step
just the same. Consenting to them separately would invite exactly the
one-character evasion Layer A had to close.
Order follows the DOCUMENT — whichever key appears first, and each flag as written. That is stable for a given manifest, which is what the consent record needs: the comparison is literal, so a reordering the developer did not make must not read as a change. (Re-ordering the keys in the manifest IS a change to what was approved, and correctly re-prompts.)
METHOD topLevelString(String key) RETURNS String
METHOD findByName(String name) RETURNS PackageRef
CLASS ModuleWorker
epm/src/ModuleWorker.ev:23
Constructors
INIT(String compilerSetting, String selfPath, boolean optimize, boolean release, boolean force, boolean noInline, boolean quiet)
Methods
MODIFY METHOD addItem(String folder, String name, boolean stage) RETURNS void
METHOD itemCount() RETURNS int64
METHOD claim(Channel[int64] work, Channel[BuildOutcome] out) RETURNS int64
Pull items until the work channel closes, building each one.
Returns how many it took, which the caller discards — the value exists so
the call has a value to bind, because a bare void call is not a statement
and a PARALLEL body can hold nothing else.
CLASS Resolver
epm/src/Resolver.ev:7
Fields
String baseDirString cacheDir
Constructors
INIT(String baseDir, String cacheDir)
Methods
MODIFY METHOD resolve(PackageRef ref, String anchorDir) RETURNS ResolvedDep
METHOD triedPathsFor(PackageRef ref, String anchorDir) RETURNS String[]
METHOD joinPath(String a, String b) RETURNS String
MODIFY METHOD resolveGraph(Manifest root) RETURNS STATUS
MODIFY METHOD visit(String requirer, PackageRef ref, String[] ancestors, String anchorDir) RETURNS STATUS
METHOD nodeIndex(String name) RETURNS int64
METHOD ancestorsContain(String[] ancestors, String name) RETURNS boolean
MODIFY METHOD validateCandidate(String folder, String name, String version) RETURNS boolean
CLASS Semver
epm/src/Semver.ev:14
Constructors
INIT()
Methods
METHOD parseVersion(String s) RETURNS | SemVersion
METHOD compare(SemVersion a, SemVersion b) RETURNS int32
METHOD parseRange(String s) RETURNS | VersionRange
METHOD satisfies(String version, String range) RETURNS | boolean
METHOD selectHighest(String[] versions, String range) RETURNS | String
METHOD makeMatchAll() RETURNS VersionRange
METHOD caretRange(PartialSpec p) RETURNS VersionRange
METHOD tildeRange(PartialSpec p) RETURNS VersionRange
METHOD parseComparatorSet(String s) RETURNS | VersionRange
METHOD contains(SemVersion v, VersionRange r) RETURNS boolean
METHOD parsePartial(String s) RETURNS PartialSpec
METHOD parseIntStrict(String s) RETURNS | int32
METHOD comparePre(String a, String b) RETURNS int32
METHOD isNumericIdent(String s) RETURNS boolean
METHOD numCompare(String a, String b) RETURNS int32
METHOD lexCompare(String a, String b) RETURNS int32
METHOD charIndex(String s, char32 c, int64 from) RETURNS int64
METHOD splitOn(String s, char32 sep) RETURNS String[]
CLASS Toolchain
epm/src/Toolchain.ev:21
Constructors
INIT()
Methods
METHOD isLoaded() RETURNS boolean
METHOD error() RETURNS String
METHOD profileName() RETURNS String
METHOD ccPath() RETURNS String
METHOD cxxPath() RETURNS String
METHOD libraryExt() RETURNS String
METHOD ldPath() RETURNS String
The PINNED linker. Resolved by evbuild via xcrun -f ld, never taken from
PATH: a clang with no pinned toolchain links with whatever ld comes
first, and on this machine that is Anaconda's ld64-530 (2022), which
cannot parse the DWARF 5 both current clangs emit — so -g silently
yields a binary whose debug info the system linker discards.
METHOD findEvbuild(String selfPath) RETURNS String
Locate epm/tools/evbuild.py. Mirrors Builder.findCompilerScript:
anchor on EPM'S OWN LOCATION, never on the module being built — a user's
project lives anywhere, and anchoring on it walks up to / and finds
nothing.
MODIFY METHOD load(String selfPath, String profileWanted, String libFile) RETURNS boolean
Run evbuild config --json and take the resolved toolchain from it.
METHOD compilePrefix(String lang) RETURNS String[]
The compile argv prefix for one unit's language: driver, base flags,
then the object flags (-c -fPIC). Include paths and the source come
from the plan and are appended by the caller.
The compiler and its flags for one unit's language.
ll returns COMPLETE and does not fall through to the shared objflags,
which is the whole reason it is a named arm rather than a default. An
.ll handed to the C++ arm compiles to a correct object and exits 0 —
it does not fail — while silently dropping -MMD (unused on IR) and
overriding the module's target triple with the host's. A wrong answer
that returns success is the shape worth spending a branch on.
METHOD sonameArgsResolved() RETURNS String[]
-install_name @rpath/<lib> on Mach-O, -Wl,-soname,<lib> on ELF.
Already SUBSTITUTED by evbuild — epm asks for the library by name via
--lib and receives finished arguments. Deliberately not done here: the
governing invariant is that epm never CONSTRUCTS a compiler argument, and
splicing a filename into a flag template is constructing one. It also
keeps the {lib} token's meaning in exactly one place, where make's
renderer already reads it.
CLASS Workspace
A WORKSPACE is a named set of module folders that epm build --all builds
together, resolving their union graph and walking it in dependency order.
It exists to answer one question the toolchain Makefile no longer can:
"rebuild everything after a git pull or an accidental rm -rf." The
per-module make targets used to do that, and the workspace model deletes them:
a module is built because it is a MEMBER here, never because someone added a
target for it.
── Why the members are WRITTEN DOWN and not discovered ──────────────────────
Discovery-by-glob was considered and rejected on evidence. The Envzn repo root
holds ~45 directories of which ~15 are modules, and Manifest.findManifestPath
accepts ANY .json carrying a "name" field — so probes/, datatest_spike/,
compiler-battery/, detector-wiring/ and golden-compiler/ all look like
modules to it. A workspace that discovered its members would build the
graveyard. Cargo writes [workspace] members down for the same reason.
The list is a SET, deliberately unordered: epm resolves each member's graph and computes the build order. Encoding order in the file would reintroduce the accumulation problem the per-module make targets had.
── The file ─────────────────────────────────────────────────────────────────
The name is a PREFIX, not a constant: one repo may carry several workspaces
(envzn-workspace.json for the language repo's own modules,
data-science-workspace.json for the AdvancedMath→stats→mlearn chain that
ships as a separate bundle). epm build --all uses "envzn"; --workspace <n>
selects another.
Member paths are relative to the file's own directory, so a workspace is relocatable — the same file works in the repo and in an extracted bundle.
epm/src/Workspace.ev:39
Constructors
INIT()
Methods
MODIFY METHOD find(String startFolder, String name) RETURNS boolean
Locate <name>-workspace.json by walking UP from startFolder, so the
command works from anywhere inside the tree rather than only at its root.
Bounded at 24 levels rather than "until the parent stops changing": a fixed ceiling cannot spin on a filesystem that reports an odd root. (Same shape as Builder.findCompilerScript, and for the same reason.)
MODIFY METHOD read(String path) RETURNS void
Read the members array from the workspace file at path. A missing file,
a missing "members" key, or a non-string entry each yield no members —
the caller reports "no members", which is the honest message either way.
METHOD memberPath(int64 index) RETURNS String
A member's folder as an absolute path — <rootDir>/<member>. Members are
written relative to the workspace file so the set relocates with it.
INTERFACE ICommand
epm/src/interfaces.ev:7
Methods
METHOD run(String[] args) RETURNS int32
STRUCT ExternalDep
epm/src/structs.ev:124
Fields
String nameString versionString linkString formString frameworkString includeRelString libdirRelString versionMacroString[] librariesString[] headersString[] defines
STRUCT GitHubRepo
epm/src/structs.ev:55
Fields
String userString repo
STRUCT LockEntry
epm/src/structs.ev:21
Fields
String nameString versionString sourceString pathString gitUrlString shaString[] approvedFlags
STRUCT NativeDep
epm/src/structs.ev:159
Fields
String nameString varNameString pkgConfigString brewString[] candidatesString installBrewString installApt
STRUCT PackageRef
epm/src/structs.ev:9
Fields
String nameString versionString pathString gitUrl
STRUCT PartialSpec
epm/src/structs.ev:104
Fields
boolean okint32 majorint32 minorint32 patchint64 depthString prerelease
STRUCT Resolution
epm/src/structs.ev:142
Fields
String nameString prefixString versionString rungboolean found
STRUCT ResolvedDep
epm/src/structs.ev:44
Fields
String nameString versionString pathString sourceboolean found
STRUCT SemVersion
epm/src/structs.ev:72
Fields
int32 majorint32 minorint32 patchString prerelease
STRUCT Tag
epm/src/structs.ev:61
Fields
String nameString commitSha
STRUCT VersionRange
epm/src/structs.ev:83
Fields
boolean matchAllboolean hasLowerint32 loMajorint32 loMinorint32 loPatchString loPreboolean loInclusiveboolean hasUpperint32 hiMajorint32 hiMinorint32 hiPatchString hiPreboolean hiInclusiveboolean allowPrerelease