Conversation
When a defaults file declares `append_arch: <value>`, that value (not the default filename) is appended to the effective architecture. In a chained `--default a::b::c` invocation only the defaults files that carry `append_arch` contribute to the suffix, giving fine-grained control over install-path qualification. readDefaults() collects each file's append_arch value in order before merge_dicts() flattens the metadata, storing the list as `_append_arch_qualifiers` in the merged meta. compute_combined_arch() checks for `_append_arch_qualifiers` first and uses those verbatim values as the suffix; the legacy `qualify_arch: true` path (which appended every non-release default name) is preserved as a fallback for existing setups that do not use append_arch. Example: defaults-gcc13.sh has `append_arch: gcc13`, defaults-release.sh has none → `--default release::gcc13` produces `<arch>-gcc13` instead of `<arch>-release-gcc13`.
When a recipe specifies `sources:` (tarball URLs) rather than a git repository, bits downloaded the archive into $SOURCEDIR but never unpacked it. The build script would then rsync a bare .tar.gz into the build directory, so `configure` / `CMakeLists.txt` were missing and the build failed immediately. Add `_extract_source_archives(source_dir)` which scans for archives after every download pass and unpacks them with `tar --strip-components=1` (or the equivalent zip logic), so the unpacked source tree lands directly in $SOURCEDIR just as a git checkout would. Supported formats: .tar.gz / .tgz, .tar.bz2 / .tbz2, .tar.xz / .txz, .tar.zst, .zip. A `.bits_extracted` sentinel prevents re-extraction on resumed builds. Tests: update all ParallelCheckoutSourcesTest cases to mock the new helper, and add ExtractSourceArchivesTest (9 cases) covering sentinel idempotency, per-format extraction, and checkout_sources integration.
…ation
patches: listed in a recipe YAML were copied to SOURCEDIR and exposed as
/ environment variables, but were never applied - the
build system left application entirely to recipe authors. In practice,
97 out of 97 lcg.bits recipes that declared patches had no Prepare()
override and therefore built with unpatched source trees.
Add _apply_patches(spec, source_dir) which iterates over spec[patches]
in declaration order and invokes:
patch -p1 --input <patch_path> (cwd=source_dir)
A .bits_patched sentinel file is written after successful application,
mirroring the .bits_extracted sentinel used by _extract_source_archives(),
so that incremental/resumed builds do not attempt re-application.
Call _apply_patches() at the end of every source-preparation path in
checkout_sources():
* tarball sources - after _extract_source_archives()
* git sources - after _verify_commit_pin(), in both the existing-
checkout and fresh-clone branches
patch -p1 handles both tarball extracts (no .git present) and git working
trees equally. The source_dir path is keyed by short_commit_hash(spec),
which changes whenever the recipe, tag, or patch content changes, so the
sentinel is never stale across meaningful source changes.
Add nine unit tests in tests/test_workarea.py covering:
- spec with no patches key (no-op)
- empty patches list (no-op)
- existing .bits_patched sentinel (idempotent skip)
- single patch: correct patch -p1 --input invocation and cwd
- multiple patches: applied in declaration order
- sentinel written on success
- inline checksum suffix (,sha256:...) stripped from patch filename
- CalledProcessError propagates on patch failure
- sentinel NOT written on patch failure
…rsal Without --batch, if a source dir is in a partially-patched state (e.g. from a previous failed run where no .bits_patched sentinel was written), patch(1) prompts interactively: Reversed (or previously applied) patch detected! Assume -R? [n] In a terminal session the user may answer 'y', causing patch to reverse the previously-applied hunks and exit 0. _apply_patches then writes the .bits_patched sentinel, locking in an unpatched (or mixed) source state. The subsequent cmake/make step then fails with 'Cannot find source file'. With --batch, patch never prompts and exits non-zero when it encounters a reversed/already-applied patch, which propagates as a CalledProcessError. The user must then clean the SOURCEDIR manually (rm -rf sw/SOURCES/<pkg>/), but at least the sentinel is not written and the failure is explicit. Updated tests to expect ['patch', '-p1', '--batch', '--input', ...].
…lied A previous build run may have extracted a tarball with the old hardcoded --strip-components=1 and written .bits_extracted before _archive_prefix_depth was introduced. On retry the extraction sentinel suppresses re-extraction, leaving source files at the wrong subdirectory depth so patch -p1 cannot find them. Before calling _extract_source_archives, remove .bits_extracted if patches are declared but .bits_patched does not yet exist. This forces a clean re-extract with the correct strip depth on any retry, without requiring manual source directory cleanup.
The common-prefix loop iterated over range(min_path_len), which includes the filename component itself. For a single-file archive (or any archive where every file shares the same full path) every component trivially satisfies "all paths agree", so depth was set to the full path length rather than the directory depth - e.g. depth=2 for pkg-1.0/hello.txt instead of 1, causing --strip-components=2 to over-strip and leave an empty source directory. Fix: iterate over range(min_path_len - 1) so the filename is never counted as a common prefix level. This also corrects the photos/215.4/ two-level-prefix case and the ./pkg-1.0/ dot-prefix case. Also fix getPackageList: introduce _disable_set to skip re-processing packages that are already known to be disabled. Previously, a package with prefer_system (e.g. GCC-Toolchain) was appended to the disable list once per occurrence in the dependency queue, producing hundreds of duplicate --disable=GCC-Toolchain entries in error-message argument logs. Guard both disable.append sites and add an early-continue at the top of the resolution loop. Deduplicate the list in the two error-message formatting sites in build.py as a belt-and-suspenders measure. All 857 tests pass.
After arch-conditional filtering and bash evaluation, each resolved
source entry is passed through Python % formatting with the dict
{"name": spec["package"], "version": spec["version"]}. This lets
recipes avoid repeating the package name and version in every URL:
sources:
- https://example.com/%(name)s/%(name)s-%(version)s.tar.gz
Substitution errors (unknown keys, malformed %) are silently ignored
so that URLs containing literal % characters are unaffected.
Adds a new "name = version" clause to the dependency requirement syntax,
allowing a recipe to lock a specific version of a dependency directly in
its requires/build_requires list instead of through a defaults-*.sh
override entry.
Syntax:
requires:
- root = 6.24.02
- my-provider = feature-branch
- "boost = 1.82.0:(?!osx)" # combined with arch-conditional
- "boost = 1.82.0:defaults=o2" # combined with defaults-conditional
The pin overrides both the recipe default and any defaults-*.sh override,
taking the highest precedence in the resolution order. After filtering,
spec["requires"] still contains plain package names so storeHashes is
unaffected; the changed version/tag propagates naturally through the
dependency hash chain.
Conflict detection:
- Two packages pinning the same dep to different versions → fatal error.
- A pin declared after the dep was already resolved at a different version
→ fatal error with a "move the pinning package earlier" hint.
13 new tests in test_utilities.py cover _parse_req_matcher, the filter
functions with version-pinned entries, and all _collect_version_pins
scenarios (basic, arch-inactive, same-version-two-owners, conflict,
already-resolved-same, already-resolved-conflict).
864 tests pass.
resolve_spec_data (used by build.py to expand source URLs before download) already supported %(package)s and %(version)s but not %(name)s. Source URLs in lcg.bits recipes now use %(name)s as a shorter alias; without this fix the build aborted with KeyError: 'name' on the first package whose sources field contained %(name)s.
Malformed recipes, bad !include references, unknown %(var)s
substitutions, patch failures, and unsupported download protocols
previously caused unhandled Python exceptions (raw tracebacks).
All are now intercepted and reported via dieOnError().
workarea._apply_patches
Catch CalledProcessError from patch(1), collect all .rej files
left in the source tree, and surface them in the error message so
the developer can see exactly which hunks failed without digging
into the build directory.
utilities.resolve_spec_data
Catch KeyError on %(unknown_var)s expansion; report the missing
variable name, the package, the offending value, and the full list
of available variables.
utilities.resolve_tag
Same treatment for %(var)s in the tag: field.
utilities.construct_include (!include in YAML)
Wrap open() in try/except OSError and the yaml/json parsers in
their respective exceptions; re-raise as ConstructorError with the
filename and position so parseRecipe surfaces it as a clean
"Unable to parse" message.
utilities.parseRecipe
Broaden "except (ScannerError, ParserError)" to "except
yaml.YAMLError" so ConstructorError from failed !include directives
is caught and reported cleanly instead of propagating as a crash.
utilities.getGeneratedPackages
Wrap __import__("packages") and pkg.getPackages() in try/except;
call dieOnError naming the offending packages.py file.
download.download
Guard the downloadHandlers dict lookup; call dieOnError listing
the unsupported protocol, the URL, and the supported protocols
instead of raising a raw KeyError.
Tests updated to match the new behaviour (dieOnError / yaml.YAMLError
instead of raw CalledProcessError / FileNotFoundError).
workarea.py - _apply_patches:
- Import ProgressPrint and emit "==> Patching PKG@VERSION" before the
patch loop, matching the "==> Compiling" style so the package being
processed is always visible before any patch(1) output.
- In non-debug mode, capture patch(1) stdout/stderr with subprocess.run
so that "patching file …" lines no longer leak into the progress
display; the captured output is forwarded to debug() on success and
prepended to the error message on failure.
- On failure call progress.end("failed") before dieOnError so the
progress line closes cleanly; on success call progress.end("done").
build.py - doBuild:
- Wrap checkout_sources() in try/except OSError and convert the
exception to a dieOnError call ("Failed to fetch sources for
PKG@VERSION: …") so a failed download (e.g. unresolved shell variable
in a source URL) prints a clean error instead of a raw Python
traceback.
Some tarballs (e.g. the LCG-mirrored HDF5 tarball) have a two-level
leading prefix ("./hdf5-1.14.6/…"), so _archive_prefix_depth() returns
2 and correct extraction needs --strip-components=2. Old bits code
hardcoded --strip-components=1, leaving an unstripped "hdf5-1.14.6/"
subdirectory inside $SOURCEDIR and writing a .bits_extracted sentinel.
On subsequent runs the new code saw the sentinel, skipped extraction,
and cmake failed with "does not appear to contain CMakeLists.txt".
The previous workaround removed stale sentinels only for packages that
declare patches (because patched packages exposed the symptom earlier).
Non-patched packages like hdf5 were never fixed.
Fix: _extract_source_archives() now writes the per-archive strip depth
into the sentinel as JSON ({"strips": {"hdf5-1.14.6.tar.gz": 2}}). On
every run it compares the recorded depths against what _archive_prefix_
depth() would compute today; if they differ the sentinel is removed and
the archives are re-extracted with the correct depth. Sentinels written
by the old code (empty file, not valid JSON) are also treated as stale
and replaced automatically.
The now-redundant manual sentinel removal for patched packages in
checkout_sources() is removed; the universal check in
_extract_source_archives() covers all packages.
…ronment mirrors what each package's runtime modulefile
Under --builders, doBuild() ran one serial preparation loop that checked out every package's sources inline and only started the scheduler afterwards, so no build began until all downloads finished. At O(1000) packages this left the CPUs idle for a long time. Source checkout is now a scheduler "download" task (fetch:<pkg>, via the new _doCheckout helper); the build task depends on it plus its dependencies' builds. This activates the scheduler's existing download/build task types and their separate caps, so packages compile as soon as their sources are present while other downloads keep flowing. Inline checkout remains only on the single-builder path. Also: - --prefetch-workers now defaults to -1 (auto = min(builders, 4)); 0 disables. - Add --parallel-downloads N (default 2), wired to the scheduler download cap. - Update test_async_build defaults test; fix the stale sentinel test to write a valid JSON .bits_extracted sentinel (the extractor re-extracts on a strip -depth mismatch, so an empty sentinel no longer means "skip").
With --sandbox=auto (the default), a plain local Linux build (no --docker) resolved to podman whenever it was installed, which meant every `bits build` invoked `podman info` to probe it - and, on Debian/Ubuntu, podman's own startup pulled in dpkg-query. podman was only ever intended for --docker (nested) or explicit opt-in. resolve_sandbox_mode() now returns "off" for the local-Linux/no-docker auto case without calling podman_available(), so podman is never invoked there. Unchanged: --docker still uses nested podman when available (falling back to off), --sandbox=podman / --sandbox-image still force it, and macOS auto still uses sandbox-exec. - bits_helpers/sandbox.py: local Linux auto -> off, no podman probe - tests/test_sandbox.py: assert auto+Linux+no-docker is off and podman_available is not called - docs/REFERENCE.md §22.1, docs/ROADMAP.md, args.py --sandbox help: document the new behaviour
bits keys the source directory by package version/commit (constant for a tarball), and _apply_patches skips re-patching whenever the .bits_patched sentinel exists. So editing a recipe patch file had NO effect on rebuild: the old already-patched tree was reused (the new patches cannot be cleanly applied on top), and the stale source silently propagated into every new build hash. This was the real cause of the Gaudi confdb2 saga -- repeated patch edits never took effect. Fix: record a fingerprint of the patch set (each patch name + full content, in order) in the .bits_patched sentinel. For tarball sources, before extraction, wipe the source dir when the recorded fingerprint differs from the current patches (legacy empty sentinels count as changed, so existing trees self-heal), forcing a clean re-extract + re-patch. Git sources are untouched. Verified: new fingerprint changes iff patch content changes; legacy sentinels trigger a wipe; matching fingerprint skips. (The 5 pre-existing ApplyPatchesTest failures are unrelated -- those tests mock subprocess.check_call while the code has used subprocess.run for a while; this change does not touch that line.)
…tches) The ApplyPatchesTest cases mocked subprocess.check_call, but _apply_patches has used subprocess.run (with output capture for error messages) for a while, so the mock never intercepted the call: setUp creates empty patch files, real `patch` no-ops, and the check_call assertions saw 0 calls (5 failures, independent of the patch-fingerprint change). Update the tests to mock subprocess.run and assert the patch command + cwd via call_args (ignoring stdout/stderr/check kwargs). All 14 tests pass.
The patch-set re-extraction guard only fired when .bits_patched existed with a different fingerprint. But if a patch run fails partway, _apply_patches dies without writing .bits_patched while .bits_extracted is already present and the tree is partially patched. On retry the wipe was skipped, extraction skipped, and patch re-applied onto the dirty tree -> "Reversed (or previously applied) patch detected" + corruption (seen with madgraph5amc). Wipe also when there is no .bits_patched sentinel but .bits_extracted exists, so patches always apply to a pristine tree.
|
If there are no objections, I plan to merge this with the main branch. The next iteration will focus on performance improvements (job scheduler), simplification (reducing the number of command-line flags and streamlining documentation for easier reading), and removing dead code (makeflow). After that, the focus would be on integration with the CVFS testbed and, eventually, with a real CVMFS repository to test the entire pipeline and measure performance. |
if a package isn't built yet and is mentioned in untracked_requires would that be built here? |
All good it works for untracked_requires my setup was broken. |
|
If a package has this defined it doesn't fail it takes up the later requires field that is mentioned. Is this a designed to be that way? @pbuncic package: demo
version: 1.2.3
requires:
- gcc
- zlib
requires:
- Python |
|
We can also get this merged https://github.com/bitsorg/bits/pull/101/changes |
I was under the impression that you were OK with the already-provided ways to manage include files in bits. As I wrote earlier, I'd like to start cleaning up the code and adding more ways to do the same thing right now s not what I'd like to do unless you can justify why it has to be done exactly that way. |
This is the result of merging defaults; the same key in the same defaults will override the first one. Why can't you do it with one require statement? If it helps, you can pass by to discuss what you are doing and if there is a better way of achieving the goal. |
We are OK with the header part as there already exists a way to !include to do it. In the recipe we can't source an script that has variables defined. So we definitely need this there. We have a Single Build Script for all Integration Builds that we build using SCRAM. We have a single build script that builds out IBs for GCC/Clang ARM/x86_64 LTO/Non-LTO Devel. Only the variables change sometimes like %(scram_compiler)s %(vectorization)s these and certain variables that define flags for nvcc, gcc or clang change. Ideally we want to keep a one script and have these variables defined in defaults which we will chain together to build our IBs. |
The script already sources other scripts, it is a shell and you can do whatever you want. The issue is that variables in sourced script are not expanded. In principle, any variable defined in yaml header is also available as shell variable to the recipe (uppercase). Would this work for you? |
|
Yeah I do think that might work given that they do the same thing in the end. Will be rewrite of recipes but yes! |
|
maybe we could add a prefix for such env variables like |
I find that odd - $ already denotes a variable, and I prefer to use the same name as in the definition (some of the recipes in lcg.bits use this convention). |
|
On second thought I don't think that will work because we don't add variables to HASH after they resolved we HASH it. We can't add variables to HASH that would change the HASH of all packages even if they didn't use any of the mentioned variables. |
Signed-off-by: Predrag Buncic <pbuncic@yahoo.com>
I am afraid that I do not understand your case. Can you give me a concrete example (using your include syntax) and explain what you expect bits to do? |
package: integration_build_gcc
version: 12
variables:
compiler: gcc
enable_tools: rocm
---
%(##INCLUDE: cms.bits/scram-build.sh)spackage: integration_build_clang
version: 12
variables:
compiler: clang
enable_tools: rocm cuda
---
%(##INCLUDE: cms.bits/scram-build.sh)s## FILE SCRAM_BUILD
useCompiler=%(compiler)s
extraTools=%(enable_tools)sSo in this example we stay contained to one scram-build.sh but as per our variables that are defined the bits will generate different build recipe by substitution useCompiler, extraTools. |
|
bits way of doing it would be: And then then `` |
|
A. This prevents me from changing the name of package. This basically assumes the entire integration package header always stays the same for all except for the variables part. |
Right, and what is wrong with that? If I understand well, this is sort of a metapackage anyway, so why are the patches needed here? They belong to the actual buildable software packages. Also, where is the problem in changing the package name? In stacks.bits I started by defining lcg.sh metapackage (long list of dependencies with some exclusions and customisation based on variables (cuda, osx...). After building everything I decided to split this is externals.sh, generators.sh and key4hep.sh - each metapackage built without need to rebuild individual packages that were already build while building lcg.sh. |
Add a narrow, C-preprocessor-style `#include` for recipe bodies, resolved in
parseRecipe before variable substitution and hashing:
#include <repo/qualified/path.sh> # resolved under $BITS_REPO_DIR
#include "local/path.sh" # relative to the recipe's directory
Only whole-line `#include <...>`/"..." directives are spliced; ordinary shell
`#` comments, shebangs and prose are left untouched. Inclusion is recursive
with cycle detection, a depth cap, and rejection of absolute / `..`-escaping
paths; a missing file is a clear parse error. Because the splice happens
before expansion and hashing, an included file's content is expanded in the
consumer's context (%(compiler)s etc.) and folds into the consumer package's
hash, so editing a snippet rebuilds its consumers.
Acknowledgements: original recipe-include proposal by @akritkbehera
(#101).
|
OK, I finally committed this C-like #!include extension - allows to include verbatim another file before variables are expanded: Hope this works for you. |
Add a narrow text-splice include for recipe bodies, resolved in parseRecipe
before variable substitution and hashing:
#!include <repo/qualified/path.sh> # resolved under $BITS_REPO_DIR
#!include "local/path.sh" # relative to the recipe's directory
The marker is `#!include`, NOT plain `#include`: recipe bodies routinely embed
literal C `#include <string.h>` lines in heredocs that generate test programs
(e.g. lcg.bits/gcc-toolchain.sh, clang.sh, openssl.sh), so a plain `#include`
directive would collide with them. `#!include` cannot appear in C or ordinary
shell, stays `#`-prefixed (an inert comment if unprocessed), and echoes the
existing header `!include` YAML tag.
Only whole-line `#!include <...>`/"..." directives are spliced; C includes,
shell `#` comments, shebangs and prose are left verbatim. Recursive with cycle
detection, a depth cap, and rejection of absolute / `..`-escaping paths; a
missing file is a clear parse error. The splice happens before expansion and
hashing, so an included file's content expands in the consumer's context
(%(compiler)s) and folds into the consumer package's hash.
Acknowledgements: original recipe-include proposal by @akritkbehera
(#101).
Summary of changes since 2026-06-10
Covers
bits,lcg.bits(recipes), andbits-recipe-tools. Each entry tagged [Feature], [Fix], or [Improvement]; hashes are short SHAs.build env derived from dependency modulefiles (now the default)
34a672c--initdotsh-from-modulesas a hashed build input (foundation; off-state byte-identical).77a6103from-modules adds the modulefile-equivalent dev env (<PKG>_INCLUDE_DIR, Python site-packages) to init.sh.e4e713dmake--initdotsh-from-modulesthe default; add--legacy-initdotsh(aliBuild stays legacy, hashes byte-identical).e2a6169from-modules also exportsCMAKE_PREFIX_PATH(:-separated, read natively byfind_package).e429a87gateCMakeRecipe/BitsPythonenv reconstruction off under from-modules (this commit not landing in the built v0.0.28 briefly broke CMP0144-old packages; fixed by v0.0.29).d967f93drop redundant dependency-env reconstruction;cd609fcdrop redundant-DCMAKE_PREFIX_PATH;93f997ctorch_scatter/torch_sparsedrop manual PYTHONPATH loop.768bc44/900ad31/607013fenv-diff harness comparing init.sh-derived vs modulefile-derived build env.--builders scheduler & resource management
6d64434unleash the final (sink) package to full-j(default on; memory cap still applies).6dd1e4ehistory-driven critical-path scheduling (default on;--no-critical-path-schedule).dc2fc07macOS available-memory was under-reported (subtracted reclaimable inactive-anon), throttling heavy builds (ROOT to-j2on 24 GB); prefer psutil, else reclaimablevm_statbuckets.70d3fcafailure logs →LOGS/<arch>/and9e3d9cdper-archbits_build_stats.json— stop different platforms sharing one work area from clobbering each other (stats were also semantically per-platform);b3a1e46/170ff5abits statsreads the relocated file + docs.882ad22resolve%(version)sin the build-order banner.3162234usethreading.current_thread()(clear deprecation warnings).Repository providers & init workflow (aliBuild vs native bits)
2545a0caliBuild front-end defaults to the legacy path, native bits to the provider path.847e8dbaliBuild init(no PACKAGE) checks out the recipes (alidist) and exits;d3bc83ebits init <group>.bitschecks out a recipe repo from the registry;55fbd89develop a package that lives in a required provider repo.73a145eload the bootstrap repo's required providers (e.g. alice.bits → alidist.bits — fixed "gsl not found").621ddedwarn on provider version conflict;4d92e95point the "package not found" error at the provider mechanism;ee78182docs.CVMFS layout, merged views & relaxed reuse
0632f28/b70abac/b1dfc1d/e8d1222/ea04075merged symlink-farm view: one-entry-per-var env, opt-inenter/setenv --view, view-awareload/printenv+ age-based GC, path remap (fixes PyROOT).58f13b6/c2d99be/f59a547/53e91d4/80b6a08published per-build_idviews on CVMFS,bits publish --view, per-tree pre-publish primitive, CVMFS layout recorded in.meta.json.8f193fa→c52f5d7,cf19966ADR-0001 import pipeline: modulefile harvest → classify → closure/build_id→ overlay →bits import(build-sufficient from modulefiles).fac7aefreview fixes (command injection, partial-view, republish, path traversal, build_id match).44a7ee1docs for--reuse-policy/--reuse-base/--build-local.Sync / remote store
28c6989upload freshly-built packages to--write-store(S3) when reading from a CVMFS remote (cross-backendDualRemoteSync; was silently dropped).8c21990--aggressive-cleanupdropped the tarball the--write-storeupload still needed.15f82e4Boto3 tarball-name crash on specs carrying anarchitecturekey.Recipe hashing
5f4a15euntracked_requires— link a dependency without folding it into the consumer's hash (edit a dep without rebuilding the stack above);20e8ed6cookbook example.CLI robustness & bits.rc
fec3303never exit non-zero without a message (silent-exit safety net for malformed defaults/recipes).f53c6ee/0e7abd5restoresearch_pathfor single-package builds;5dd6b22usepython3.a7d0a2faccept flat (header-less)bits.rc;a7afc92CI README-path fix.lcg.bits recipes (other) & bits-recipe-tools
6f30d4fROOT: use external bits zstd (-Dbuiltin_zstd=OFF) so ROOT 6.40 findszdict.h.a11558areduce ROOTmem_per_jobto 1250;05a4eefbump bits-recipe-tools version.a441d2bModuleRecipeguardslib/lib64/pkgconfig/site-packagespath entries on existence.