Skip to content

Devel#98

Merged
pbuncic merged 117 commits into
mainfrom
devel
Jun 10, 2026
Merged

Devel#98
pbuncic merged 117 commits into
mainfrom
devel

Conversation

@pbuncic

@pbuncic pbuncic commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

bits: concurrent-build scheduling, graceful CPU prioritisation, repository providers, recipe/provenance features, and a docs overhaul

Summary

This branch brings bits from serial, single-recipe-repo builds to a parallel, resource-aware build system with pluggable recipe repositories, richer recipe/provenance features, and a rewritten documentation set.

44 commits; ~5k lines added across bits_helpers/ with matching test coverage.

Highlights

1. Concurrent build scheduling and resource awareness (--builders)

  • --builders N builds N packages at once through the built-in scheduler, respecting the dependency graph.
  • Builder-aware $JOBS: each package's parallel-compile budget is divided across the builders (effective_jobs) so the concurrent jobs together never oversubscribe the machine's CPU/load.
  • Memory budgeting: recipes can declare mem_per_job (peak RSS per compile process) and mem_utilisation; the scheduler's ResourceManager admits builds so total memory stays within budget.
  • Self-tuning stats: per-package CPU/RSS traces are recorded (--resource-monitoring) and auto-loaded on the next run to schedule with up-to-date estimates.
  • Download/build overlap: source checkouts run as scheduler download tasks that overlap with compilation; prefetch is on by default and --parallel-sources fetches multiple sources: URLs concurrently.

2. Graceful CPU prioritisation: --build-nice ladder + straggler watchdog

  • --build-nice (on by default for --builders > 1; opt out with --no-build-nice) staggers concurrent builds across OS priority: one runs at top priority (full speed), the rest are progressively backed off, and the freed top slot is taken over as builds finish — so contention degrades gracefully instead of all builds fighting equally. Native builds use nice; --docker/podman builds use the cgroup-equivalent --cpu-shares.
  • Straggler-renice watchdog: a long single compile (e.g. a heavy Fortran/C++ translation unit) that was backed off can drag out the end of a build. A watchdog boosts the longest-running straggler back to top priority, one at a time, after --build-nice-boost-after seconds (default 600):
    • Native builds: the straggler's process subtree is reniced toward 0.
    • --docker builds: each build runs in a named container; the watchdog finds the long-running compiler back-end with docker exec … ps and renices it via docker exec --user 0 … renice (run as root inside the container, so it can raise priority without host privilege). If the image lacks ps, it warns once and disables that path — see operational notes.

3. Scheduler robustness

  • Fixed an end-of-build hang: a build task raising SystemExit (as dieOnError does) was not caught by the worker's except Exception, killing the worker silently and leaving the job stuck "running" forever so the build never finished and printed no summary. Workers now catch BaseException, and the master loop has a bounded wait + watchdog that fails stuck jobs if all workers exit — guaranteeing the build always finishes with a summary.

4. Build-error interception and diagnostics

  • Real exit code propagated: the post-build status check read $PIPESTATUS after the test had clobbered it, so every failure exited 1. It now captures ${PIPESTATUS[0]} first, preserving the real failing code at all three recipe-run sites.
  • Error excerpt in failure messages: BUILD FAILED now includes the matched high-signal error lines (compiler/linker/cmake/make/python) plus the log tail, so triage no longer means opening and grepping the full log.

5. Repository providers

  • A recipe marked provides_repository: true pulls an entire additional recipe repository from git at dependency-resolution time and adds it to BITS_PATH, enabling modular and nested recipe sets. always_load: true clones it unconditionally at startup; repository_position and --provider-policy control search order (with prepend downgraded unless explicitly granted, to prevent recipe-controlled PATH hijacking). The provider's commit hash folds into every dependent's build hash for reproducibility.
  • Bootstrap: bits build PKG with no local recipe dir clones bits-providers (default or --organisation <org>) and the recipe repo it points to. Provider repo version is selectable via the recipe's tag: or an @tag suffix on the providers URL.

6. Recipe authoring and defaults-profile features

  • Defaults-profile variables in recipe bodies: %(VAR)s-style placeholders resolved from the active defaults profile, with soft expansion so unknown placeholders don't hard-fail.
  • Version pinning: pin a dependency's version from a recipe's requires (name = version, with optional :matcher), in addition to the existing defaults-*.sh overrides: mechanism (which can also set tag/source).
  • %(name)s / %(version)s substitution in sources: URLs and patch names.
  • Generic build-time PKG_CONFIG_PATH so the build environment mirrors each package's runtime modulefile; shared arch-detection helper; per-default append_arch.

7. Source checkout and patching (workarea)

  • Auto-apply patches declared in a recipe's patches: after source preparation (patch --batch to avoid interactive reversal).
  • Correct re-extraction: sources are re-extracted when the patch set changes or a previous patch run left a dirty tree / stale sentinel; fixes for strip-level and single-file-archive prefix-depth (--strip-components) bugs.

8. Provenance and integrity

  • Manifest schema v3 records the resolved sources, patches, and expanded variables for each package; builds can be replayed with --from-manifest.
  • Source checksums: --check/enforce/print/write-checksums plus per-profile/per-recipe checksum policy; store integrity verification of recalled tarballs.

9. Sandbox and miscellaneous robustness

  • Don't probe/require podman for local Linux builds; clean, actionable error messages replacing raw crashes; ==> Patching PKG@VERSION progress; download OSError interception; guard against accidental deletes.

10. Documentation

  • Rewritten and consolidated docs under docs/: USERGUIDE, REFERENCE, COOKBOOK, WORKFLOWS, ROADMAP; obsolete mkdocs site and stale pages removed. New material covers --builders/--build-nice, the three version-pinning mechanisms, repository providers, and recipe error-handling pitfalls.

New CLI flags (selected)

Flag Purpose
--builders N Build N packages concurrently.
--build-nice / --no-build-nice Staggered-priority ladder for concurrent builds. On by default for --builders > 1.
--build-nice-step N Nice increment between ladder slots.
--build-nice-boost-after SECONDS Boost a straggler compile after this long (0 disables; default 600).
--parallel-sources N Concurrent sources: downloads per checkout.
--prefetch-workers N Background tarball prefetch threads.
--resource-monitoring / --resources FILE Record / consume per-package resource stats.
--from-manifest FILE Replay a build from a manifest.
--provider-policy Control provider BITS_PATH insertion order.

New recipe / defaults fields (selected)

mem_per_job, mem_utilisation, provides_repository, always_load, repository_position; requires: name = version[:matcher] pins; overrides: may set version/tag/source; %(name)s/%(version)s/profile %(VAR)s expansion in sources, patches, and recipe bodies.

Backward compatibility

  • --builders defaults to 1 (serial), preserving prior behaviour; --build-nice only engages for --builders > 1.
  • Existing recipes and defaults files are unaffected; all new recipe/defaults fields are optional.

Operational notes

  • --docker builds require ps (the procps package) in the build image so the watchdog can find the in-container compile; if absent, it warns once and disables in-container renicing for the run (the build is unaffected). Build images used with bits build --docker should include procps.

Testing

  • New/updated unit tests: test_scheduler.py (incl. the SystemExit-hang regression), test_nice_ladder.py (ladder, native renice, docker exec renice, ps-missing fallback), test_memory.py/test_build_stats.py (builder-aware jobs, resource stats), test_repo_provider.py/test_always_on_providers.py/test_provider_staleness.py, test_manifest.py (schema v3), test_workarea.py (patching/re-extraction), test_utilities.py (version pins, variable expansion).
  • Full suite passes. The only environment-dependent items: test_sync.py S3 tests require the optional botocore dependency, and test_build.py::test_coverDoBuild is a slow, fully-mocked coverage test independent of these changes.

pbuncic and others added 30 commits May 15, 2026 21:15
  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.
pbuncic added 2 commits June 7, 2026 18:38
'bits brew' now writes next to the recipes by default (lcg.bits/macos/
Brewfile) instead of cwd/Brewfile, and doctor checks that location first.
Flavours stay command-line only (--flavour). Extend the defaults variables:
block so an entry can be GATED, and add predefined architecture variables so
gates and package requires can test the platform with the (?NAME) spelling.

* predefined_arch_vars(arch): exposes the truthy platform booleans
  osx / linux / arm64 / aarch64 / x86_64 (only the true ones, so (?osx) is
  correctly false off macOS).
* resolve_variables(variables, flavours, arch, defaults): resolves variables:
  into a flat map. A plain 'name: value' is always defined; a gated
  'name: {value: V, when: MATCHER}' is defined only when MATCHER is active.
  MATCHER reuses the requires grammar -- (?flavour), arch regex (osx/(?!osx)),
  defaults=, combined with && / || -- evaluated against the vars resolved so
  far, so a gate may reference CLI flavours, the predefined arch vars, and
  earlier ('previously defined') entries. Precedence: predefined < flavour <
  defaults entry, but a CLI flavour always overrides a defaults entry of the
  same name while staying visible to gates.

Wired into build.py and deps.py defaultsReaders (replacing the old
flavour->variables merge; flavours still go into env/hash). The resolved map
feeds the existing (?NAME) matchers that gate package requires and %(NAME)s
templating, so flavours + variables + predefined vars can all gate packages
(e.g. define heavygen when (?openloops) && (?!osx), then 'pkg:(?heavygen)').
Note: arch regexes in when: match the RAW architecture (resolution runs before
the combined-arch qualifier is computed). Tests in test_utilities.py.
@akritkbehera

Copy link
Copy Markdown
Collaborator

Is there a way to make sure certain packages are present first without adding it as an dependency? It shouldn't change the hash if it changes.

Comment thread bits_helpers/utilities.py

# A variable-reference matcher is spelled "(?NAME)" -- an identifier in the same
# parenthesised form as a regex group, but one that is NOT a legal regex (e.g.
# "(?cuda)" raises re.error: "unknown extension ?c"). This lets a recipe gate a

@akritkbehera akritkbehera Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pbuncic How can I test this seems like a useful thing to have but unable to use it.

requires:
 - cuda(?cuda)
BASH
And I need to define a variable cuda in defaults? Is that it? 

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, have a look into stacks.bits

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok ig there is an issue with it this works

package: defaults-release
version: vBits
variables:
  gcc: 'true'
  gcc-compiler: 'true'
---
package: zlib
version: 1.2.3
requires:
 - "gcc:(?gcc)"
 - "gcc-compiler:(?gcc-compiler)"
 ---

For gcc it works perfectly fine but if the name is gcc-compiler. I think '-' is causing the issue? Maybe?

raise source.error("unknown extension ?" + char,
re.error: unknown extension ?l at position 1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — names must be C-style identifiers, so a hyphen isn't allowed. Use underscores instead (gcc_compiler, not gcc-compiler).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides, I am about to merge devel into main, hope it is OK with you.

pbuncic added 13 commits June 9, 2026 18:54
The per-build 'CPU oversubscription factor N (defaults system.build_oversubscribe)'
message added noise to every build invocation. Remove it; the factor still
applies, just silently.
…tartup)

The pre-build stale .downloading sentinel cleanup did os.walk(workDir) over the
ENTIRE sw tree on every run - including INSTALLROOT and BUILD, the whole
installed stack (tens of thousands of files). That stat() storm caused a long,
visible pause between 'Updating repositories: done' and the first package's
rebuild check, especially on macOS. Sentinels are only ever created under
SOURCES/ (source archives) and TARS/<arch>/store/ (tarballs, per
resolve_store_path), so scope the walk to just those subtrees. Verified the
scoped cleanup still removes sentinels in both locations and skips INSTALLROOT.
Follow-up to d9b4243. Sentinels live at two fixed depths -
SOURCES/<pkg>/<ver>/<file>.downloading and
TARS/<arch>/store/<hash[:2]>/<hash>.downloading - so no recursive walk is needed
at all. Two non-recursive glob patterns match exactly those directories and
nothing else, eliminating even the scoped os.walk. Verified the globs catch
sentinels at both depths and skip decoys at other depths.
A recipe's full body (comments included) fed the build hash, so documentation-
only edits rehashed the package and forced a rebuild. Hash a normalized copy
instead: normalize_recipe_for_hash() drops whole-line comments and blank lines,
while preserving here-document bodies verbatim (a leading '#' there is data).
The executed recipe is unchanged. One-time effect: all hashes shift once on
upgrade; thereafter comment/whitespace edits never trigger a rebuild. Tests in
test_hashing.py cover comments, blanks, inline '#', and here-doc preservation.
The stale-sentinel cleanup now globs SOURCES/*/*/*.downloading and
TARS/*/store/*/*.downloading (commit 00836ec); the glob mock lacked those keys,
raising KeyError. Add them returning empty.
Add bits_helpers/cvmfs_catalog.py (productionised from the validated standalone
prototype): when a directory is served by a single dedicated nested catalog
rooted there, read the catalog hash from the user.catalog_counters magic xattr,
fetch the one catalog object over HTTP, decompress it, and list every entry from
a local SQLite query -- no per-file FUSE walk. Conservative: it lists only from
a catalog rooted at the path with no deeper nested catalogs, else signals
fallback (exit 3), so it can never return a partial/oversized listing.

bitsModules is the thin entry script (bitsBuild/bitsDeps family) that drives it.
The shell frontend's collectModules now enumerates <pkg>/<version> via
_listPkgVersions: on /cvmfs it tries bitsModules and falls back to the POSIX
find walk on the fallback contract; off CVMFS it goes straight to find (no
python spawn), so non-CVMFS behaviour is unchanged. Tests build a crafted
catalog SQLite and cover path reconstruction, both output modes, and the
fall-back contract.
When --defaults qualifies the architecture (qualify_arch: osx_arm64 ->
osx_arm64_gcc15), the install tree lives under the combined string but the bits
frontend auto-detects only the raw base arch, so 'bits enter'/'q' looked in the
wrong sw/<arch> (and the success banner's suggested command omitted -a).

Build: when args.architecture != raw_architecture, the success banner now prints
'bits -a <combined> enter <pkg>/...' plus the BITS_ARCHITECTURE persist hint;
unchanged when the arch was not qualified.

Frontend (bits launcher): when -a was not given and the auto-detected arch is
absent/unknown in the work dir, prefer an arch actually present -- guess it if
exactly one exists, or warn and list them when several do (instead of the old
silent 'newest wins'). An explicit -a is always respected verbatim.
…h enter

REFERENCE.md additions for recent features:
- defaults variables: (plain + gated when: form) and predefined platform vars
  (osx/arm64/aarch64) feeding the (?NAME) matcher; note that (?!osx) is an arch
  regex, not variable negation.
- macOS Homebrew system layer: homebrew_formula, bits brew / Brewfile, --brew /
  BITS_BREW on-demand install, HomebrewRecipe symlink exposure, doctor check.
- bits q fast listing on CVMFS via the serving catalog (bitsModules).
- entering a qualified-architecture build: the success-banner -a hint,
  BITS_ARCHITECTURE, and the frontend single-arch guess / multi-arch warning.
- CLI table: add --parallel-downloads, --auto-resources, --brew; fix the stale
  --prefetch-workers default (now -1 auto, was 0).
NAME must be [A-Za-z_][A-Za-z0-9_]*; a hyphen makes (?NAME) fall through to an
arch regex (gate never fires) and can't be an env var. Use underscores.
Remove the 'Build-time sandbox network allowed by default' and 'Reusing
cached provider' messages printed on every bits start. The provider one was
already redundant with the debug() line above it (kept for verbose mode).
Autodetection scanned sw/ and only filtered all-uppercase dirs, so lowercase
non-arch dirs like wrapper-scripts were offered as architecture candidates
(spurious 'several architectures present' warning). Enumerate $WORK_DIR/MODULES/*
instead — the authoritative set of module-serving architectures.
When several architectures are present under MODULES/ and none was given,
pick the most recently modified (built) one (ls -1t, newest first) instead
of warning and bailing. A short note tells the user which was chosen and how
to override with -a.
@akritkbehera

Copy link
Copy Markdown
Collaborator

Before you merge it one change would be

==> Packages will be built in the following order:
     - db6@6.2.32
     - zlib@v%(version)s
     - gmake@4.3
     - libffi@v3.5.2
     - expat@R_2_7_1
     - sqlite@version-3.48.0
     - autotools@1.5
     - bz2lib@1.0.8
     - gdbm@1.26
     - libuuid@2.40
     - xz@v%(version)s
     - Python@v%(version)s
     - cuda@12.9.1

version is not replaced in this display we can maybe just call resolve_spec_data here to replace version. So the print shows it.

pbuncic added 8 commits June 10, 2026 12:58
…Build

The display prefix / branding (BITS_PKG_PREFIX, BITS_BRANDING) and the ALICE
default no longer live in bits itself. bits q now prints native <pkg>/<version>
and only reformats to <PREFIX>@<pkg>::<version> when BITS_PKG_PREFIX is set.

- aliBuild: symlink -> standalone wrapper that exports BITS_ORGANISATION=ALICE,
  BITS_PKG_PREFIX=VO_ALICE, BITS_BRANDING=aliBuild, then execs bits. Needs no
  bits.rc; reproduces the old VO_ALICE@pkg::ver output.
- bits launcher: drop baked ALICE/VO_ defaults and the implicit VO_ prefix;
  replace the INI readBitsRc with a minimal work_dir reader. Everything else
  is read by the Python layer.
- Reject old-style bits.rc: deprecated keys (sw_dir/repo_dir/search_path/
  pkg_prefix/branding) or non-[bits] sections print the required renames and exit.
- organisation stays the registry/provider home selector and now also defaults
  from BITS_ORGANISATION env so aliBuild selects ALICE's registry for build too.
- docs: env-var table, bits q format, bits.rc format note.
At the end of a --builders run, integrate the per-package resource-monitor
traces into useful core-seconds and divide by cores x wall-clock to estimate
whole-run CPU utilisation, plus the average number of builders busy at once.
Store both (and a recommendation) under a 'tuning' key in
bits_build_stats.json, and print the recommendation when there is headroom
(<90%):

- slots mostly full but cores idle  -> per-package serial phases; suggest a
  higher --oversubscribe (memory budget unchanged by design).
- slots often empty                 -> dependency-graph bound; suggest more
  --builders and/or reusing prebuilt tarballs.

build_stats.tuning_report() is pure/unit-tested; aggregate_and_write() gains an
optional tuning= kwarg (existing callers unaffected).
…ecmd

bits q was slow on a real filesystem because it (1) ran collectModules, which
rm -rf's and re-copies every modulefile into the MODULES cache on every call,
and (2) then spawned 'modulecmd avail' to enumerate. Both are redundant for a
listing: the install tree's modulefiles are the source of truth.

Add listAvailableModules() — the collectModules loop minus the copy — which
emits <pkg>/<version> for each install dir that ships a modulefile, via the
existing _listPkgVersions fast path (CVMFS catalog shortcut included). q now
pipes it through sort; load/enter/avail keep using collectModules/modulecmd.
Instant on a normal FS (matches plain find); BASE helper module no longer
shown.
collectModules rm -rf'd MODULES/<arch> and re-cp'd every modulefile on every
load/enter/avail — O(packages) of mkdir/cp each time. Now sync incrementally:
copy a modulefile only when missing or newer than the cached copy, prune cache
entries whose source vanished, and drop emptied package dirs. After a build
with no module changes this copies nothing, so repeated load/enter are instant.

Portable: uses bash -nt tests and plain find (no GNU -printf, no bash-4
associative arrays) so it still works on macOS /bin/bash 3.2.
existModules ran 'modulecmd bash -t avail' (a full MODULES-tree scan/evaluate)
once per requested module just to verify it exists -- the main reason
load/enter/printenv/setenv were notably slower than q. Check the modulefile
cache directly instead (<pkg>/<version> is a file, bare <pkg> a directory),
falling back to modulecmd only on a miss so extra MODULEPATH entries and
default-version aliases still resolve. A '..' guard keeps the fast path from
matching path-traversal names. The actual 'modulecmd load' is unchanged.
Set BITS_TIMING=1 to print the wall time of each phase (preamble,
collectModules, existModules, modulecmd) to stderr, to localize where
load/enter/printenv spend their time. Uses bash EPOCHREALTIME (>=5) or GNU
date; no overhead when unset.
BITS_TIMING showed collectModules was the whole cost of load/enter/printenv
(~4.6s) -- even incremental, its O(N) per-package stat walk + prune + rmdir is
slow on a filesystem with high per-stat latency (install dirs are symlinks into
the hashed store). It ran on every load even when no package had changed.

Add a stamp file (.bits_sync_stamp) written after each successful sync, and
skip the entire walk when a single shallow 'find -newer' (the same ~0.02s
traversal as a bare find) reports nothing newer under the install tree.
-maxdepth 2 covers the arch dir and per-package dirs, so package add/remove and
version re-symlinks all bump a checked mtime and re-trigger the sync.

Repeat load/enter/printenv now ~0.003s instead of 4.6s; the first call after a
build still does one full sync. Also adds startup/archdetect BITS_TIMING marks.
…utput

Arch autodetection is now MODULES-first: when -a is not given, pick from the
installed MODULES/<arch> trees directly. A single installed arch (the common
case) is used with no bitsBuild python spawn, reclaiming the ~0.29s archdetect
phase seen under BITS_TIMING. Python is only spawned to disambiguate several
installed arches (prefer the host's) or when nothing is installed yet.

-q (which already unsets VERBOSE) now also exports MODULES_VERBOSITY=silent, so
setenv/printenv/load/enter no longer emit environment-modules' 'Loading <pkg>' /
'Loading requirement:' lines -- output can be captured/parsed cleanly. Help
updated for enter/setenv.
@pbuncic
pbuncic merged commit 8ba03c7 into main Jun 10, 2026
12 checks passed
@pbuncic

pbuncic commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Sorry, @akritkbehera, I didn't see your comment before merging. I do not see such a problem in my builds. I normally derive the version from the tag, like:

package: ROOT
description: CERN ROOT data analysis framework
version: "%(tag_basename)s"
tag: "v6-38-00"

Where do you set the version in your recipe?

@pbuncic

pbuncic commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Is there a way to make sure certain packages are present first without adding it as an dependency? It shouldn't change the hash if it changes.

This I do not understand, can you clarify?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants