Skip to main content

Products

A product is a named capability a package offers. Consumers ask for the name, such as cmake or doctest_cpp_h, and envy answers with a concrete path or value. Nothing hardcodes a cache path, so the cache can stay content-addressed and disposable.

PRODUCTS = {
cmake = "bin/cmake" .. envy.EXE_EXT,
ctest = "bin/ctest" .. envy.EXE_EXT,
}

Values are relative to the package directory. envy resolves them against pkg/ when queried.

A product is not necessarily a script

Executable products get wrapper scripts deployed. But a product can be any named value: a header path for a header-only library, a shared library, a data directory, an apt package name. Mark those script = false. They deploy nothing, and consumers reach them through envy product <name> or envy.product(name). Header-only packages need no bin-directory tricks.

Declaring products​

Table with string values, for executables​

PRODUCTS = { gn = "gn" .. envy.EXE_EXT }

The string shorthand means { value = <string>, script = true }.

Table with table values, for everything else​

PRODUCTS = {
doctest_cpp_dir = { value = ".", script = false },
doctest_cpp_h = { value = "doctest.h", script = false },
libjlink = { value = "libjlinkarm.so", script = false, platforms = { "linux" } },
}
FieldMeaning
valueRequired string. A path relative to the package directory, or a raw value for user-managed packages.
scriptDeploy a wrapper script for it. Defaults to true.
platformsRestrict the product, for example { "linux" }.

Directory products and file products are both common. A build system usually wants the include directory, while a dependency edge wants the file.

Function, computed from options​

PRODUCTS(opts) returns the table. Use it when the layout depends on an option, or to generate one product per list element:

-- One product per requested tool.
PRODUCTS = function(opts)
local result = {}
for _, tool in ipairs(opts.tools) do
result[tool] = tool .. envy.EXE_EXT
end
return result
end
-- Claim the bare `python3` name only if this project asked for it, so several
-- interpreters can coexist. Each also provides its own `python3.13`.
PRODUCTS = function(opts)
local python = ((envy.PLATFORM == "windows") and "install/" or "install/bin/")
.. "python" .. envy.EXE_EXT

return {
python = opts.provide_python and python or nil,
python3 = opts.provide_python3 and python or nil,
["python" .. opts.version:match("^(%d+%.%d+)")] = python,
}
end

Setting a key to nil omits it, which is how an option turns a product on and off.

-- Vendor layout differs enough per platform that the whole table is computed.
PRODUCTS = function(opts)
local products = ({
darwin = { aws = "aws-cli/aws" },
windows = { aws = "Amazon/AWSCLIV2/aws.exe" },
linux = { aws = "bin/aws" },
})[envy.PLATFORM]
assert(products, "unsupported platform: " .. envy.PLATFORM)
return products
end

The three consumption surfaces​

SurfaceHowWho uses it
Deployed wrappercmakehumans, Makefiles, anything expecting a normal executable
CLIenvy product cmake, envy product --jsonbuild-system glue, scripts, envy run users
Luaenvy.product("ninja")other specs, during BUILD and INSTALL

From Lua, the usual shape is a product feeding a generated script:

DEPENDENCIES = { { product = "cmake" }, { product = "ninja" } }

BUILD = function(install_dir, stage_dir, fetch_dir, tmp_dir, opts)
return envy.template([[
{{cmake}} -G Ninja -DCMAKE_MAKE_PROGRAM={{ninja}} -S . -B build
{{ninja}} -C build
]], { cmake = envy.product("cmake"), ninja = envy.product("ninja") })
end

envy.product returns the provider's absolute path, so the script never names a cache path. It resolves declared dependencies only, and only once the dependency is ready for the phase you call from. An undeclared name is an error rather than a lookup, and a product declared needed_by = "build", the default, cannot be resolved during FETCH. See BUILD.

Asking for a product installs its provider on demand, which is why a deployed wrapper works on a machine that has never synced. The four-line script calls envy product, and the package materializes.

Resolution rules​

  • Product names are global within a project. cmake means one thing.
  • Two packages providing the same product name is an error. There are no priority rules and no tie-breaks. Options exist so that a spec can decline to claim a generic name, as in the provide_python3 example above.
  • A product whose platforms exclude the current machine is not present.
  • User-managed products are raw values rather than paths, because there is no cache directory to resolve against.

Depending on a product​

An entry can name a product instead of an identity, which says "I need whatever provides ninja" without naming a spec:

DEPENDENCIES = { { product = "ninja" }, { product = "cmake" } }

See Declaring Dependencies.

See also​