diff --git a/README.md b/README.md index 68e6401..3a36ed1 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,17 @@ Then run Bootstrap with the manifest: ./bootstrap.bash packages.manifest ``` +Multiple manifests may be supplied in one invocation: + +```bash +./bootstrap.bash core.manifest containers.manifest security.manifest +./bootstrap.bash *.manifest +``` + +Bootstrap parses, plans, and resolves every supplied manifest before executing +any package operation. If preflight fails, no package changes are made, and the +diagnostic retains the original manifest filename and line number. + To inspect the plan without making package-manager changes: ```bash diff --git a/doc/adr/ADR-049-Preflight-All-Manifests-Before-Execution.md b/doc/adr/ADR-049-Preflight-All-Manifests-Before-Execution.md new file mode 100644 index 0000000..f94a089 --- /dev/null +++ b/doc/adr/ADR-049-Preflight-All-Manifests-Before-Execution.md @@ -0,0 +1,449 @@ +# ADR-049: Preflight All Manifests Before Execution + +Date: 2026-07-11 + +## Status + +Proposed + +## Intent and Documentation Posture + +This ADR records the intended command-line and execution semantics for processing +multiple manifest files in one Bootstrap invocation. + +The decision is intentionally described in detail because error handling is part +of Bootstrap's public safety contract. Future implementation work should preserve +both the selected behavior and the reasoning behind it rather than treating +multiple-manifest support as a simple argument-parsing convenience. + +## Context + +Bootstrap currently accepts one manifest file per invocation. Users who divide +their desired packages into focused manifests, such as core, containers, +security, or language-specific manifests, must invoke Bootstrap repeatedly. + +A natural command-line extension is to accept multiple manifest paths: + +```bash +bootstrap.bash core.manifest containers.manifest security.manifest +``` + +Shell glob expansion should also work without Bootstrap implementing its own +file discovery: + +```bash +bootstrap.bash *.manifest +``` + +Concatenating files before passing them to Bootstrap is not an equivalent +solution: + +```bash +cat core.manifest containers.manifest | bootstrap.bash - +``` + +When standard input is used, the parser sees `-` as the input source. An error +can therefore be reported only against that synthetic source and its combined +line number. The diagnostic cannot reliably identify which original file +contained the error or which line number applied within that file. + +Human-friendly diagnostics are a central benefit of Bootstrap. Errors should +identify the actual manifest path and original line number whenever that +information is available. Multiple-manifest support must preserve that +provenance through parsing, planning, resolution, explanation, and reporting. + +The initial discussion considered behavior similar to invoking Bootstrap once +per file. That approach preserves provenance, but a direct sequence such as: + +```bash +bootstrap.bash a.manifest && bootstrap.bash b.manifest +``` + +would execute the first manifest before the second manifest had been parsed or +planned. A malformed later manifest could therefore be discovered only after +an earlier manifest had already modified the system. + +The preferred model is stronger. Bootstrap should first preflight the complete +set of supplied manifests. Execution should begin only when every unignored +error condition has been resolved successfully. + +The discussion also identified a legitimate future need for explicitly +permissive modes. A user may prefer to report and skip an individual package +failure or abandon one failed manifest while continuing with other manifests. +Those behaviors should be possible only through explicit command-line options. +They must not weaken Bootstrap's conservative default. + +## Decision Drivers + +The decision is guided by the following concerns: + +- Diagnostics should identify the actual manifest filename and original line + number associated with an error. +- No system changes should occur before Bootstrap knows whether the complete + requested run can be parsed, planned, and resolved. +- Existing single-manifest behavior should remain compatible. +- Input order should remain explicit and deterministic. +- The default should stop on the first error and avoid partial system changes. +- More permissive behavior should require affirmative user intent. +- Reported errors should remain visible and understandable even when the user + permits processing to continue. +- The implementation should compose the existing parser, planner, resolver, + executor, and reporting stages rather than introduce a parallel pipeline. +- Exit status should distinguish complete success from partial or incomplete + completion. + +## Decision + +Bootstrap shall accept one or more manifest operands in a single invocation. + +Each manifest shall retain its own source identity. Records and diagnostics +produced from a manifest shall preserve the original manifest path and line +number throughout the pipeline. + +Bootstrap shall process multiple manifests using two broad phases: + +1. Preflight all supplied manifests. +2. Execute eligible resolved actions only after preflight permits execution. + +Preflight includes, at minimum: + +1. Reading each manifest in command-line order. +2. Parsing and validating each manifest. +3. Planning the requested actions. +4. Resolving those actions for the selected package-manager backend. + +By default, the first error encountered during preflight shall stop the run. +Bootstrap shall execute no actions from any supplied manifest when an +unignored preflight error occurs. + +If every supplied manifest completes preflight successfully, Bootstrap shall +execute the resulting resolved actions in manifest argument order and in the +established action order within each manifest. + +The default behavior is therefore conceptually: + +```text +preflight(a, b, c) && execute(a, b, c) +``` + +It is deliberately safer than either concatenating the manifests or invoking +Bootstrap independently for each file. + +### Global Options + +Operational options such as `--dry-run`, `--explain`, `--verbose`, `--quiet`, +and `--package-manager` shall apply to the complete invocation and therefore to +every supplied manifest. + +Manifest paths shall be processed in the order received from the shell. +Bootstrap shall not implement implicit manifest discovery or reorder operands. + +### Standard Input + +The `-` operand shall continue to represent standard input. + +Because standard input cannot normally be replayed and has no original file +identity, at most one `-` operand should be accepted in one invocation. Its +source shall remain `-`, and diagnostics shall use line numbers from that input +stream. + +Users who require original filename provenance should supply manifest paths +rather than concatenate files into standard input. + +### Conservative Default + +Without an explicit continuation option, any error shall stop processing and +prevent execution. + +This includes errors associated with: + +- manifest access; +- syntax or validation; +- action planning; +- platform or package-manager resolution; and +- execution. + +Execution failures necessarily occur after the preflight barrier. By default, +an execution failure shall stop further execution and cause the overall run to +fail. + +### Explicit Continuation Policies + +Future or accompanying CLI work may provide two independent continuation +policies: + +```text +--continue-on-package-error +--continue-on-manifest-error +``` + +These names are preferred over `--ignore-*` forms because the errors are not +ignored. Bootstrap still explains, records, and reports them. The option changes +whether the error halts subsequent work. + +The precise spelling remains part of the CLI design and may be refined before +implementation, but the semantic separation is architectural. + +#### Package-Level Continuation + +When package-level continuation is enabled, a failure attributable to one +package may be reported and skipped while Bootstrap continues processing other +eligible packages. + +Package-level failures may arise during parsing, planning, resolution, or +execution. An implementation shall distinguish failures that can be safely +isolated to one package from failures that invalidate the enclosing manifest or +runtime state. + +A package-level option shall not require Bootstrap to guess the intended meaning +of malformed or ambiguous input. If a failure cannot be safely isolated to one +package record, it remains a manifest-level or run-level failure. + +#### Manifest-Level Continuation + +When manifest-level continuation is enabled, a failure that invalidates one +manifest may be reported, that manifest may be excluded from execution, and +preflight may continue with later manifests. + +Successful manifests may become eligible for execution only after preflight has +completed for the full invocation and no remaining unignored run-level failure +exists. + +Manifest-level continuation shall not merge a failed manifest's partial plan +with executable plans from successful manifests. + +#### Combined Continuation + +When both continuation policies are enabled: + +- safely isolated package failures may be reported and omitted; +- an unrecoverable manifest failure may exclude that manifest; +- later manifests may still be preflighted; and +- execution may proceed only for the surviving resolved actions after the + complete preflight phase. + +The hierarchy is: + +```text +run + manifest + package +``` + +Each continuation policy relaxes failure handling only at its stated level. + +### Exit Status + +Continuation does not convert an incomplete run into complete success. + +If any package or manifest error occurred, the overall command should return a +nonzero status even when an explicit continuation option allowed additional +work to proceed. + +The final exit-code mapping may be specified separately, but callers must be +able to distinguish: + +- complete success; +- successful completion with reported and skipped work; +- preflight failure with no execution; and +- execution failure after preflight. + +A future explicit option could request success status for acknowledged skipped +errors, but that behavior is not part of this decision and shall not be implied +by continuation alone. + +### Reporting + +Diagnostics shall remain human-readable and shall identify the narrowest useful +scope available: + +- run-level errors; +- manifest path; +- original manifest line number; +- package requirement, when known; +- pipeline stage that detected the error; and +- whether processing stopped, skipped a package, skipped a manifest, or + continued under an explicit policy. + +Dry-run and explain modes should make the global preflight boundary visible. +They should distinguish actions by source manifest and explain that execution +was either prohibited or intentionally omitted. + +## Considered Alternatives + +### Accept Only One Manifest + +Bootstrap could retain the existing one-manifest CLI and require users to invoke +it repeatedly. + +This preserves the current implementation and provenance behavior, but it +creates repetitive command lines and does not provide a global preflight safety +barrier across a related manifest set. + +### Concatenate Manifests Before Parsing + +Bootstrap or the user could concatenate all input files and process the result +as one logical manifest. + +This is mechanically simple and permits one parser invocation. It was rejected +because it loses or complicates original source provenance. Error messages may +refer only to `-`, a temporary combined file, or a combined line number that is +not directly useful to the user. + +Concatenation also blurs manifest boundaries, making file-level continuation and +reporting difficult. + +### Execute Each Manifest Immediately + +Bootstrap could loop over manifest operands and run the existing complete +pipeline for each one before moving to the next. + +This preserves filenames and line numbers and may require relatively little +orchestration work. It was rejected as the default because an early manifest +could modify the system before a later manifest's parse, planning, or resolution +failure was discovered. + +### Parse and Plan All Manifests, Then Resolve During Execution + +Bootstrap could establish a global barrier after parsing and planning while +deferring package-manager resolution until each manifest is about to execute. + +This reduces retained preflight state, but it allows a later resolution error to +be discovered after earlier execution has begun. Resolution is therefore part +of preflight for this decision. + +### Merge All Action Records into One Plan + +Bootstrap could preserve source metadata while combining every manifest's +Action Records into a single plan and resolved action stream. + +This may simplify global optimization or duplicate elimination. It was not +selected as the required model because merging can obscure manifest boundaries, +complicate manifest-level continuation, and introduce policy questions about +cross-file ordering and duplicate requirements. + +An implementation may use a combined internal stream only if source provenance, +manifest boundaries, deterministic ordering, and continuation semantics remain +fully preserved. + +### Silently Continue After Errors + +Bootstrap could report errors and continue by default. + +This was rejected because partial provisioning can be difficult to detect and +may leave a system in an unexpected state. Conservative fail-fast behavior +remains the default. + +### Treat Continued Errors as Success + +Bootstrap could return status zero whenever it completed all work that remained +after skipped failures. + +This was rejected because automation would be unable to distinguish a complete +bootstrap from a partial one. Continued work and successful work are not the +same outcome. + +### Use One General `--continue-on-error` Option + +A single option could permit all failures to be skipped when possible. + +This is convenient but too broad. Package-level and manifest-level failures have +different safety boundaries and consequences. Separate policies make user +intent more explicit and reduce surprising behavior. + +## Consequences + +### Positive + +- Users can compose focused manifests in one explicit invocation. +- Errors retain actual filename and line-number provenance. +- The complete manifest set is checked before package changes begin. +- Dry-run and explain behavior can present a coherent run-wide preflight view. +- Existing parser, planner, resolver, executor, and reporting concepts remain + reusable. +- Conservative behavior remains the default. +- Advanced users can eventually request partial progress at a clearly defined + package or manifest boundary. +- Automation can detect partial completion through nonzero exit status. + +### Negative + +- Bootstrap must retain or otherwise manage the preflight results for multiple + manifests until execution begins. +- Temporary-file ownership and cleanup become more complex. +- The CLI context must represent an ordered collection of manifest paths rather + than one manifest path. +- Parser, planner, and resolver code may need richer error accumulation to + support package-level continuation. +- Manifest-level and package-level summaries add reporting complexity. +- Duplicate or conflicting package requirements across manifests remain a + policy question unless explicitly defined elsewhere. +- A large manifest set may consume more temporary storage during preflight. +- Continuation policies increase the test matrix substantially. + +### Implementation Impact + +The intended design should extend the existing layered pipeline rather than +replace it. + +Likely implementation work includes: + +- collecting an ordered list of manifest operands; +- preserving per-manifest source context; +- producing and retaining per-manifest or provenance-preserving Action Records; +- resolving all eligible actions before execution; +- representing preflight outcomes at package, manifest, and run scope; +- enforcing a global execution barrier; +- executing surviving Resolved Actions deterministically; +- aggregating human-readable summaries and exit status; and +- adding regression tests for default and continuation behavior. + +Package-level continuation may require deeper parser, planner, resolver, or +executor changes than basic multiple-manifest support. It may therefore be +implemented after the global preflight and manifest-level behavior, provided the +public design remains consistent with this ADR. + +## Open Questions and Follow-Ups + +The following details require additional design or implementation validation: + +- The final spelling of the package-level and manifest-level continuation flags. +- Whether the first implementation should include continuation policies or only + establish the conservative global preflight behavior. +- The exact exit codes used for partial completion and multiple simultaneous + failure categories. +- Whether preflight should report only the first error by default or collect + additional errors while still refusing execution. +- Which malformed-line failures can be safely isolated to a package under + package-level continuation without guessing user intent. +- Whether duplicate requirements across manifests are preserved independently, + coalesced, or diagnosed. +- How conflicting version constraints across manifests are reported. +- Whether execution summaries should be per manifest, aggregate, or both. +- Whether a manifest-level continuation policy permits execution when every + manifest failed and therefore no actions survive. +- Whether an explicit option should ever convert acknowledged skipped errors to + a successful exit status. + +These questions do not change the central decision: preserve provenance, +preflight the complete invocation before execution, fail conservatively by +default, and require explicit policy for partial progress. + +## Related Decisions + +- Related to ADR-013: Fail Conservatively and Avoid Surprising System Changes +- Related to ADR-014: Separate Manifest Parsing from Package Installation +- Related to ADR-016: Provide Dry-Run and Explain Modes for Planned Changes +- Related to ADR-021: Layer the Bootstrap Engine Around Well-Defined + Responsibilities +- Related to ADR-023: Prefer Explicit Configuration Over Implicit Discovery +- Related to ADR-024: Provide a Stable and Explicit Command-Line Interface +- Related to ADR-025: Provide Human-Centered Logging with Progressive Levels of + Detail +- Related to ADR-028: Favor the Principle of Least Surprise +- Related to ADR-033: Prefer Composition Over Special Cases +- Related to ADR-039: Test Observable Behavior Rather Than Implementation +- Related to ADR-040: Prefer Deterministic Behavior +- Related to ADR-047: Represent Planned Bootstrap Operations as Immutable Action + Records +- Related to ADR-048: Execution SHALL Consume Only Resolved Actions diff --git a/doc/cli.md b/doc/cli.md index 39170af..f5074e3 100644 --- a/doc/cli.md +++ b/doc/cli.md @@ -28,6 +28,7 @@ bootstrap.bash --help bootstrap.bash --version bootstrap.bash --dry-run bootstrap.bash --dry-run packages.txt +bootstrap.bash --dry-run core.manifest security.manifest bootstrap.bash --dry-run - bootstrap.bash --package-manager apt bootstrap.bash --package-manager apk @@ -82,11 +83,23 @@ This placeholder exists only until later roadmap phases introduce manifest parsing, planning, and package execution. -## Manifest argument +## Manifest arguments -When a manifest path is provided, Bootstrap parses package requirements from -that manifest and uses the same parser, planner, resolver, and executor pipeline -for dry-run and execution modes. +When one or more manifest paths are provided, Bootstrap parses, plans, and +resolves the complete ordered set before beginning execution. If any manifest +fails during this preflight phase, Bootstrap reports the original filename and +line number, returns a non-zero status, and performs no package changes. + +For example: + +```bash +bootstrap.bash core.manifest containers.manifest security.manifest +bootstrap.bash *.manifest +``` + +Manifest order is the order supplied by the shell. Bootstrap does not discover, +sort, or merge manifest files. Records retain their original source paths and +line numbers through planning, resolution, dry-run explanation, and execution. A manifest path of `-` follows the common Unix convention of reading manifest content from standard input: @@ -96,7 +109,10 @@ printf '%s\n' git curl shellcheck | bootstrap.bash --dry-run - ``` This is useful for generated manifests, shell pipelines, and temporary package -lists that do not need to be written to a separate file. +lists that do not need to be written to a separate file. Standard input may be +specified at most once per invocation. Because its source name is `-`, users who +need per-file provenance should pass manifest paths instead of concatenating +files into standard input. ## `--help` @@ -196,8 +212,6 @@ Unsupported options fail conservatively with a human-readable diagnostic and a non-zero exit status. This avoids silently accepting misspelled flags or implying that future roadmap options already work. -Unexpected positional arguments also fail conservatively until later roadmap -phases define manifest input behavior. ## Recovery guidance diff --git a/doc/reference/bootstrap_8bash.html b/doc/reference/bootstrap_8bash.html index 5651636..1125b80 100644 --- a/doc/reference/bootstrap_8bash.html +++ b/doc/reference/bootstrap_8bash.html @@ -85,10 +85,10 @@  Runs the placeholder operation when no manifest was supplied. More...
  int bootstrap_run_dry_run_plan () - Parses, plans, resolves, and prints a read-only dry-run plan. More...
+ Preflights all manifests and prints one read-only invocation plan. More...
  int bootstrap_run_execution_plan () - Parses, plans, resolves, executes, and prints execution results. More...
+ Preflights all manifests before executing the complete resolved plan. More...
 

Detailed Description

@@ -144,21 +144,15 @@

-

Parses, plans, resolves, and prints a read-only dry-run plan.

-

This path composes the manifest parser, planner, and resolver, captures their intermediate records, and renders the result without executing resolved actions. Temporary files are removed before the function returns.

Return values
+

Preflights all manifests and prints one read-only invocation plan.

+

Dry-run uses the same complete Action Record and Resolved Action streams that execution would consume. No output plan is rendered until every manifest has crossed the global preflight barrier successfully.

Return values
- - - + + +
0The manifest was parsed, planned, resolved, and displayed successfully.
65The manifest could not be parsed or planned.
69The planned actions could not be resolved on this system.
0All manifests were preflighted and displayed successfully.
65A manifest could not be parsed or planned.
69The complete action set could not be resolved on this system.
-
Examples
bootstrap_context_set_manifest_path packages.txt
-
- -
int bootstrap_run_dry_run_plan()
Parses, plans, resolves, and prints a read-only dry-run plan.
-
int bootstrap_context_enable_dry_run()
Records that dry-run mode was requested.
-
@@ -177,20 +171,16 @@

-

Parses, plans, resolves, executes, and prints execution results.

-

This path uses the same parser, planner, and resolver pipeline as dry-run mode, then streams Resolved Actions into the executor. Dry-run mode stops before this function so inspection and execution remain separate.

Return values
+

Preflights all manifests before executing the complete resolved plan.

+

No executor is invoked until every supplied manifest has parsed, planned, and resolved successfully. The resulting Resolved Action stream preserves manifest argument order and source provenance, satisfying ADR-049 without concatenating source files or losing filename and line-number diagnostics.

Return values
- - - + + +
0The manifest was executed successfully or no actions were needed.
65The manifest could not be parsed or planned.
69The planned actions could not be resolved on this system.
0The complete manifest set executed successfully or required no work.
65At least one manifest could not be parsed or planned.
69At least one planned action could not be resolved.
70At least one resolved action failed during execution.
-
Examples
bootstrap_context_set_manifest_path packages.txt
-
-
int bootstrap_run_execution_plan()
Parses, plans, resolves, executes, and prints execution results.
-
diff --git a/doc/reference/context_8bash.html b/doc/reference/context_8bash.html index 7e83d2d..3f3c1d6 100644 --- a/doc/reference/context_8bash.html +++ b/doc/reference/context_8bash.html @@ -94,15 +94,15 @@ int bootstrap_context_enable_quiet ()  Records that quiet output was requested. More...
  +int bootstrap_context_has_manifest_paths () + Tests whether at least one manifest path was supplied. More...
+  +int bootstrap_context_get_manifest_count () + Prints the number of manifest operands in the invocation scope. More...
+  int bootstrap_context_get_package_manager ()  Prints the effective package-manager selector. More...
  -int bootstrap_context_has_manifest_path () - Tests whether a manifest path was supplied. More...
-  -int bootstrap_context_get_manifest_path () - Prints the manifest path associated with the current invocation. More...
-  int bootstrap_context_is_dry_run ()  Tests whether dry-run mode is active. More...
  @@ -126,14 +126,14 @@   String BOOTSTRAP_FLAG_QUIET   -String BOOTSTRAP_MANIFEST_PATH -  +IndexedArray BOOTSTRAP_MANIFEST_PATHS +  String BOOTSTRAP_CONTEXT_PACKAGE_MANAGER  

Detailed Description

Owns runtime option state for a bootstrap invocation.

-

The command-line parser records operational flags and the optional manifest path in this module rather than asking unrelated parts of the program to inspect raw arguments. Later roadmap phases can ask small, intention-revealing questions such as bootstrap_context_is_dry_run instead of reading global variables directly.

+

The command-line parser records operational flags and ordered manifest paths in this module rather than asking unrelated parts of the program to inspect raw arguments. Later roadmap phases can ask small, intention-revealing questions such as bootstrap_context_is_dry_run instead of reading global variables directly.

The state remains process-local Bash state. This is deliberate for the early bootstrap engine because it keeps the runtime small, inspectable, and free of external dependencies while still creating a clear boundary around invocation context.

This internal state deliberately avoids the public BOOTSTRAP_ configuration variable names accepted from .env files and the process environment. Keeping the names separate lets the loader distinguish an exported environment override from runtime state that was populated by a lower-priority source.

Function Documentation

@@ -249,14 +249,14 @@

-

◆ bootstrap_context_get_manifest_path()

+ +

◆ bootstrap_context_get_manifest_count()

- + @@ -264,17 +264,14 @@

-

Prints the manifest path associated with the current invocation.

-
Standard Output
The manifest path.
+

Prints the number of manifest operands in the invocation scope.

+
Standard Output
The number of manifest paths.
Return values

bootstrap_context_get_manifest_path bootstrap_context_get_manifest_count ( )
- +
0The manifest path was printed successfully.
0The count was printed successfully.

-
Examples
manifest_path="$(bootstrap_context_get_manifest_path)"
-
printf 'manifest: %s\n' "${manifest_path}"
-
@@ -307,14 +304,14 @@

-

◆ bootstrap_context_has_manifest_path()

+ +

◆ bootstrap_context_has_manifest_paths()

- + @@ -322,20 +319,14 @@

-

Tests whether a manifest path was supplied.

+

Tests whether at least one manifest path was supplied.

Return values

bootstrap_context_has_manifest_path bootstrap_context_has_manifest_paths ( )
- +
0A manifest path is available.
0One or more manifest paths are available.
1No manifest path was supplied.

-
Examples
- -
fi
-
int bootstrap_context_get_manifest_path()
Prints the manifest path associated with the current invocation.
-
int bootstrap_context_has_manifest_path()
Tests whether a manifest path was supplied.
-
@@ -573,19 +564,19 @@

-

◆ BOOTSTRAP_MANIFEST_PATH

+ +

◆ BOOTSTRAP_MANIFEST_PATHS

- +
BOOTSTRAP_MANIFEST_PATHBOOTSTRAP_MANIFEST_PATHS
-

Optional package manifest path supplied as the single positional argument.

-

Bash variable: read-write string

+

Ordered package manifest paths supplied as positional arguments.

+

Bash variable: read-write indexed_array

diff --git a/doc/reference/globals.html b/doc/reference/globals.html index 82dcb91..a937f7c 100644 --- a/doc/reference/globals.html +++ b/doc/reference/globals.html @@ -80,9 +80,9 @@

- b -

  • bootstrap_context_enable_explain() : context.bash
  • bootstrap_context_enable_quiet() : context.bash
  • bootstrap_context_enable_verbose() : context.bash
  • -
  • bootstrap_context_get_manifest_path() : context.bash
  • +
  • bootstrap_context_get_manifest_count() : context.bash
  • bootstrap_context_get_package_manager() : context.bash
  • -
  • bootstrap_context_has_manifest_path() : context.bash
  • +
  • bootstrap_context_has_manifest_paths() : context.bash
  • bootstrap_context_is_dry_run() : context.bash
  • bootstrap_context_is_quiet() : context.bash
  • bootstrap_context_is_verbose() : context.bash
  • @@ -100,7 +100,7 @@

    - b -

    • BOOTSTRAP_FLAG_EXPLAIN : context.bash
    • BOOTSTRAP_FLAG_QUIET : context.bash
    • BOOTSTRAP_FLAG_VERBOSE : context.bash
    • -
    • BOOTSTRAP_MANIFEST_PATH : context.bash
    • +
    • BOOTSTRAP_MANIFEST_PATHS : context.bash
    • bootstrap_planner_plan_manifest_records() : planner.bash
    • bootstrap_print_help() : bootstrap.bash
    • bootstrap_print_version() : build-metadata.bash
    • diff --git a/doc/reference/globals_func.html b/doc/reference/globals_func.html index 1048ddc..2ec245f 100644 --- a/doc/reference/globals_func.html +++ b/doc/reference/globals_func.html @@ -78,9 +78,9 @@

      - b -

      diff --git a/doc/reference/search/all_1.js b/doc/reference/search/all_1.js index cf34fe7..c9e195b 100644 --- a/doc/reference/search/all_1.js +++ b/doc/reference/search/all_1.js @@ -16,9 +16,9 @@ var searchData= ['bootstrap_5fcontext_5fenable_5fexplain_13',['bootstrap_context_enable_explain',['../context_8bash.html#a79909f8c22da4ea0fe3d436f739d63f0',1,'context.bash']]], ['bootstrap_5fcontext_5fenable_5fquiet_14',['bootstrap_context_enable_quiet',['../context_8bash.html#a59ee2b959693c335b4f03d958e6d13c2',1,'context.bash']]], ['bootstrap_5fcontext_5fenable_5fverbose_15',['bootstrap_context_enable_verbose',['../context_8bash.html#a339274b487e7065911aab93f379a926f',1,'context.bash']]], - ['bootstrap_5fcontext_5fget_5fmanifest_5fpath_16',['bootstrap_context_get_manifest_path',['../context_8bash.html#a22634c7425e44ab5012662efd120757e',1,'context.bash']]], + ['bootstrap_5fcontext_5fget_5fmanifest_5fcount_16',['bootstrap_context_get_manifest_count',['../context_8bash.html#a7a5f35242977193e461edf5d68e9a828',1,'context.bash']]], ['bootstrap_5fcontext_5fget_5fpackage_5fmanager_17',['bootstrap_context_get_package_manager',['../context_8bash.html#abec1f637817eceb704fdff196433a4cc',1,'context.bash']]], - ['bootstrap_5fcontext_5fhas_5fmanifest_5fpath_18',['bootstrap_context_has_manifest_path',['../context_8bash.html#abaea340027ad7bbeed1b9028d6181f1f',1,'context.bash']]], + ['bootstrap_5fcontext_5fhas_5fmanifest_5fpaths_18',['bootstrap_context_has_manifest_paths',['../context_8bash.html#ad0a7dc049df1836470fcfb3c672039b6',1,'context.bash']]], ['bootstrap_5fcontext_5fis_5fdry_5frun_19',['bootstrap_context_is_dry_run',['../context_8bash.html#ad861bd9761dc3bcb6761628b4a3ce7fb',1,'context.bash']]], ['bootstrap_5fcontext_5fis_5fquiet_20',['bootstrap_context_is_quiet',['../context_8bash.html#a4d1d1dd1052fb65b34fbe0a7fd14c02b',1,'context.bash']]], ['bootstrap_5fcontext_5fis_5fverbose_21',['bootstrap_context_is_verbose',['../context_8bash.html#a22545b09ebf96c5b907285725fca263e',1,'context.bash']]], @@ -36,7 +36,7 @@ var searchData= ['bootstrap_5fflag_5fexplain_33',['BOOTSTRAP_FLAG_EXPLAIN',['../context_8bash.html#a2f4c34f846a66b8a81991f523299c731',1,'context.bash']]], ['bootstrap_5fflag_5fquiet_34',['BOOTSTRAP_FLAG_QUIET',['../context_8bash.html#ae5e42230aebb93d609f1fd4d2d32d119',1,'context.bash']]], ['bootstrap_5fflag_5fverbose_35',['BOOTSTRAP_FLAG_VERBOSE',['../context_8bash.html#a8724808926e2bc1f9951651a0fdae67b',1,'context.bash']]], - ['bootstrap_5fmanifest_5fpath_36',['BOOTSTRAP_MANIFEST_PATH',['../context_8bash.html#a3f09465b28178e504c7ee77a18cde06c',1,'context.bash']]], + ['bootstrap_5fmanifest_5fpaths_36',['BOOTSTRAP_MANIFEST_PATHS',['../context_8bash.html#a4c22bd55ea2a8ffe436a6946ebcc159a',1,'context.bash']]], ['bootstrap_5fplanner_5fplan_5fmanifest_5frecords_37',['bootstrap_planner_plan_manifest_records',['../planner_8bash.html#a1ce5e75f944350718e584e47dff5c078',1,'planner.bash']]], ['bootstrap_5fprint_5fhelp_38',['bootstrap_print_help',['../bootstrap_8bash.html#a2c0b2dc8a651739c341e71898b6f340d',1,'bootstrap.bash']]], ['bootstrap_5fprint_5fversion_39',['bootstrap_print_version',['../build-metadata_8bash.html#a11f8e6fc2bbde36b5d86d8608e26f3af',1,'build-metadata.bash']]], diff --git a/doc/reference/search/functions_0.js b/doc/reference/search/functions_0.js index 177a48f..4273b4e 100644 --- a/doc/reference/search/functions_0.js +++ b/doc/reference/search/functions_0.js @@ -12,9 +12,9 @@ var searchData= ['bootstrap_5fcontext_5fenable_5fexplain_9',['bootstrap_context_enable_explain',['../context_8bash.html#a79909f8c22da4ea0fe3d436f739d63f0',1,'context.bash']]], ['bootstrap_5fcontext_5fenable_5fquiet_10',['bootstrap_context_enable_quiet',['../context_8bash.html#a59ee2b959693c335b4f03d958e6d13c2',1,'context.bash']]], ['bootstrap_5fcontext_5fenable_5fverbose_11',['bootstrap_context_enable_verbose',['../context_8bash.html#a339274b487e7065911aab93f379a926f',1,'context.bash']]], - ['bootstrap_5fcontext_5fget_5fmanifest_5fpath_12',['bootstrap_context_get_manifest_path',['../context_8bash.html#a22634c7425e44ab5012662efd120757e',1,'context.bash']]], + ['bootstrap_5fcontext_5fget_5fmanifest_5fcount_12',['bootstrap_context_get_manifest_count',['../context_8bash.html#a7a5f35242977193e461edf5d68e9a828',1,'context.bash']]], ['bootstrap_5fcontext_5fget_5fpackage_5fmanager_13',['bootstrap_context_get_package_manager',['../context_8bash.html#abec1f637817eceb704fdff196433a4cc',1,'context.bash']]], - ['bootstrap_5fcontext_5fhas_5fmanifest_5fpath_14',['bootstrap_context_has_manifest_path',['../context_8bash.html#abaea340027ad7bbeed1b9028d6181f1f',1,'context.bash']]], + ['bootstrap_5fcontext_5fhas_5fmanifest_5fpaths_14',['bootstrap_context_has_manifest_paths',['../context_8bash.html#ad0a7dc049df1836470fcfb3c672039b6',1,'context.bash']]], ['bootstrap_5fcontext_5fis_5fdry_5frun_15',['bootstrap_context_is_dry_run',['../context_8bash.html#ad861bd9761dc3bcb6761628b4a3ce7fb',1,'context.bash']]], ['bootstrap_5fcontext_5fis_5fquiet_16',['bootstrap_context_is_quiet',['../context_8bash.html#a4d1d1dd1052fb65b34fbe0a7fd14c02b',1,'context.bash']]], ['bootstrap_5fcontext_5fis_5fverbose_17',['bootstrap_context_is_verbose',['../context_8bash.html#a22545b09ebf96c5b907285725fca263e',1,'context.bash']]], diff --git a/doc/reference/search/variables_0.js b/doc/reference/search/variables_0.js index da972df..f23fe45 100644 --- a/doc/reference/search/variables_0.js +++ b/doc/reference/search/variables_0.js @@ -13,6 +13,6 @@ var searchData= ['bootstrap_5fflag_5fexplain_10',['BOOTSTRAP_FLAG_EXPLAIN',['../context_8bash.html#a2f4c34f846a66b8a81991f523299c731',1,'context.bash']]], ['bootstrap_5fflag_5fquiet_11',['BOOTSTRAP_FLAG_QUIET',['../context_8bash.html#ae5e42230aebb93d609f1fd4d2d32d119',1,'context.bash']]], ['bootstrap_5fflag_5fverbose_12',['BOOTSTRAP_FLAG_VERBOSE',['../context_8bash.html#a8724808926e2bc1f9951651a0fdae67b',1,'context.bash']]], - ['bootstrap_5fmanifest_5fpath_13',['BOOTSTRAP_MANIFEST_PATH',['../context_8bash.html#a3f09465b28178e504c7ee77a18cde06c',1,'context.bash']]], + ['bootstrap_5fmanifest_5fpaths_13',['BOOTSTRAP_MANIFEST_PATHS',['../context_8bash.html#a4c22bd55ea2a8ffe436a6946ebcc159a',1,'context.bash']]], ['bootstrap_5fversion_14',['BOOTSTRAP_VERSION',['../build-metadata_8bash.html#aea89ca8732527aa21a863d2701e15bed',1,'build-metadata.bash']]] ]; diff --git a/lib/backend/apk.bash b/lib/backend/apk.bash index ab45b57..e7e2ced 100644 --- a/lib/backend/apk.bash +++ b/lib/backend/apk.bash @@ -32,7 +32,6 @@ bootstrap_backend_apk_is_available() { command -v apk >/dev/null 2>&1 } - ## @fn bootstrap_backend_apk_package_exists() ## @brief Reports whether APK knows about a package in configured repositories. ## @details diff --git a/lib/backend/apt.bash b/lib/backend/apt.bash index b355617..2ec7e24 100644 --- a/lib/backend/apt.bash +++ b/lib/backend/apt.bash @@ -16,7 +16,6 @@ ## backend modules rather than teaching the resolver package-manager-specific ## command syntax. - ## @fn bootstrap_backend_apt_is_available() ## @brief Reports whether the APT backend can inspect packages on this system. ## @details @@ -42,7 +41,6 @@ bootstrap_backend_apt_is_available() { command -v dpkg >/dev/null 2>&1 } - ## @fn bootstrap_backend_apt_package_exists() ## @brief Reports whether APT knows about a package in configured repositories. ## @details @@ -86,7 +84,6 @@ bootstrap_backend_apt_package_exists() { bootstrap_backend_diagnostic_package_unavailable apt "${package}" } - ## @fn bootstrap_backend_apt_candidate_version() ## @brief Prints the APT candidate version for a package. ## @details @@ -126,7 +123,6 @@ bootstrap_backend_apt_candidate_version() { printf '%s\n' "${candidate}" } - ## @fn bootstrap_backend_apt_dpkg_operator() ## @brief Translates a manifest version operator into a dpkg comparison operator. ## @details @@ -166,7 +162,6 @@ bootstrap_backend_apt_dpkg_operator() { esac } - ## @fn bootstrap_backend_apt_package_satisfies_version() ## @brief Checks whether the APT candidate satisfies a version constraint. ## @details diff --git a/lib/backend/backend.bash b/lib/backend/backend.bash index 15fe22e..1a97d0e 100644 --- a/lib/backend/backend.bash +++ b/lib/backend/backend.bash @@ -18,7 +18,6 @@ ## in this file, rather than by adding package-manager branches throughout the ## planner or resolver. - ## @fn bootstrap_backend_detect_package_manager() ## @brief Detects the supported package backend for the current environment. ## @details @@ -57,7 +56,6 @@ bootstrap_backend_detect_package_manager() { bootstrap_backend_diagnostic_no_supported_manager } - ## @fn bootstrap_backend_supports_capability() ## @brief Reports whether a backend advertises a named package capability. ## @details @@ -129,7 +127,6 @@ bootstrap_backend_supports_capability() { esac } - ## @fn bootstrap_backend_package_exists() ## @brief Checks whether the selected backend can find a requested package. ## @details diff --git a/lib/executor/executor.bash b/lib/executor/executor.bash index 317ad52..a8ff14d 100644 --- a/lib/executor/executor.bash +++ b/lib/executor/executor.bash @@ -114,7 +114,6 @@ bootstrap_executor_execute_resolved_action() { esac } - ## @fn bootstrap_executor_execute_resolved_actions() ## @brief Executes Resolved Actions from standard input. ## @details diff --git a/lib/runtime/context.bash b/lib/runtime/context.bash index 3b3eaa5..f0e96ff 100644 --- a/lib/runtime/context.bash +++ b/lib/runtime/context.bash @@ -2,8 +2,8 @@ ## @file lib/runtime/context.bash ## @brief Owns runtime option state for a bootstrap invocation. ## @details -## The command-line parser records operational flags and the optional manifest -## path in this module rather than asking unrelated parts of the program to +## The command-line parser records operational flags and ordered manifest paths +## in this module rather than asking unrelated parts of the program to ## inspect raw arguments. Later roadmap phases can ask small, ## intention-revealing questions such as bootstrap_context_is_dry_run instead of ## reading global variables directly. @@ -34,9 +34,9 @@ BOOTSTRAP_FLAG_VERBOSE=false ## True when the user requested non-essential output suppression. BOOTSTRAP_FLAG_QUIET=false -## @var BOOTSTRAP_MANIFEST_PATH -## Optional package manifest path supplied as the single positional argument. -BOOTSTRAP_MANIFEST_PATH="" +## @var BOOTSTRAP_MANIFEST_PATHS +## Ordered package manifest paths supplied as positional arguments. +BOOTSTRAP_MANIFEST_PATHS=() ## @var BOOTSTRAP_CONTEXT_PACKAGE_MANAGER ## Effective package-manager selector after defaults and configuration are applied. @@ -60,7 +60,7 @@ bootstrap_context_reset() { BOOTSTRAP_FLAG_EXPLAIN=false BOOTSTRAP_FLAG_VERBOSE=false BOOTSTRAP_FLAG_QUIET=false - BOOTSTRAP_MANIFEST_PATH="" + BOOTSTRAP_MANIFEST_PATHS=() BOOTSTRAP_CONTEXT_PACKAGE_MANAGER="auto" } @@ -108,28 +108,64 @@ bootstrap_context_enable_quiet() { BOOTSTRAP_FLAG_QUIET=true } -## @fn bootstrap_context_set_manifest_path() -## @brief Records the manifest path supplied by the user. +## @fn bootstrap_context_add_manifest_path() +## @brief Appends one manifest path to the ordered invocation scope. ## @details -## The CLI accepts at most one manifest path. This function stores the already -## validated positional argument so downstream code can ask for the manifest path -## without reparsing command-line input. +## Positional manifest operands are retained in the order supplied by the user. +## The preflight orchestrator later processes them in this same order so shell +## glob expansion and explicit argument ordering remain deterministic. ## -## @param path Package manifest path supplied as the positional argument. +## @param path Package manifest path supplied as a positional argument. ## @retval 0 Manifest path was recorded successfully. ## @par Examples ## @code -## bootstrap_context_set_manifest_path ./packages.txt +## bootstrap_context_add_manifest_path ./core.manifest +## bootstrap_context_add_manifest_path ./security.manifest ## @endcode -bootstrap_context_set_manifest_path() { - BOOTSTRAP_MANIFEST_PATH="$1" +bootstrap_context_add_manifest_path() { + BOOTSTRAP_MANIFEST_PATHS+=("$1") +} + +## @fn bootstrap_context_has_manifest_paths() +## @brief Tests whether at least one manifest path was supplied. +## @retval 0 One or more manifest paths are available. +## @retval 1 No manifest path was supplied. +bootstrap_context_has_manifest_paths() { + ((${#BOOTSTRAP_MANIFEST_PATHS[@]} > 0)) +} + +## @fn bootstrap_context_get_manifest_count() +## @brief Prints the number of manifest operands in the invocation scope. +## @par Standard Output +## The number of manifest paths. +## @retval 0 The count was printed successfully. +bootstrap_context_get_manifest_count() { + printf '%s\n' "${#BOOTSTRAP_MANIFEST_PATHS[@]}" +} + +## @fn bootstrap_context_get_manifest_path_at() +## @brief Prints the manifest path at a zero-based position. +## @param index Zero-based manifest position. +## @par Standard Output +## The selected manifest path. +## @retval 0 The path was printed successfully. +## @retval 1 The index was outside the recorded manifest range. +bootstrap_context_get_manifest_path_at() { + local index + + index="$1" + if ((index < 0 || index >= ${#BOOTSTRAP_MANIFEST_PATHS[@]})); then + return 1 + fi + + printf '%s\n' "${BOOTSTRAP_MANIFEST_PATHS[index]}" } ## @fn bootstrap_context_set_package_manager() ## @brief Records the package-manager selector for this invocation. ## @details ## Configuration loading and command-line parsing apply precedence before the -## resolver runs. The context stores the effective selector so dry-run and +## resolver runs. The context stores the effective selector so dry-run and ## execution paths use the same package-manager decision instead of each path ## hard-coding its own default. ## @@ -157,34 +193,6 @@ bootstrap_context_get_package_manager() { printf '%s\n' "${BOOTSTRAP_CONTEXT_PACKAGE_MANAGER}" } -## @fn bootstrap_context_has_manifest_path() -## @brief Tests whether a manifest path was supplied. -## @retval 0 A manifest path is available. -## @retval 1 No manifest path was supplied. -## @par Examples -## @code -## if bootstrap_context_has_manifest_path; then -## bootstrap_context_get_manifest_path -## fi -## @endcode -bootstrap_context_has_manifest_path() { - [[ -n "${BOOTSTRAP_MANIFEST_PATH}" ]] -} - -## @fn bootstrap_context_get_manifest_path() -## @brief Prints the manifest path associated with the current invocation. -## @par Standard Output -## The manifest path. -## @retval 0 The manifest path was printed successfully. -## @par Examples -## @code -## manifest_path="$(bootstrap_context_get_manifest_path)" -## printf 'manifest: %s\n' "${manifest_path}" -## @endcode -bootstrap_context_get_manifest_path() { - printf '%s\n' "${BOOTSTRAP_MANIFEST_PATH}" -} - ## @fn bootstrap_context_is_dry_run() ## @brief Tests whether dry-run mode is active. ## @retval 0 Dry-run mode is active. diff --git a/lib/runtime/diagnostics.bash b/lib/runtime/diagnostics.bash index 8dc7ad2..fe48ce4 100644 --- a/lib/runtime/diagnostics.bash +++ b/lib/runtime/diagnostics.bash @@ -12,7 +12,6 @@ ## quiet mode. Quiet mode is allowed to hide non-essential progress output, but ## it must not hide information required to understand a conservative failure. - ## @fn bootstrap_diagnostic_manifest_unreadable() ## @brief Reports that a manifest file cannot be read. ## @details @@ -44,7 +43,6 @@ bootstrap_diagnostic_manifest_unreadable() { return "${BOOTSTRAP_EXIT_MANIFEST}" } - ## @fn bootstrap_diagnostic_manifest_malformed_line() ## @brief Reports one malformed manifest line with recovery guidance. ## @details diff --git a/lib/runtime/recovery.bash b/lib/runtime/recovery.bash index 5a8d768..15cad98 100644 --- a/lib/runtime/recovery.bash +++ b/lib/runtime/recovery.bash @@ -35,7 +35,6 @@ bootstrap_recovery_emit() { bootstrap_log_emit 'recovery' "${message}" 'stderr' } - ## @fn bootstrap_recovery_no_supported_package_manager() ## @brief Explains next steps when no supported package manager was detected. ## @details @@ -59,7 +58,6 @@ bootstrap_recovery_no_supported_package_manager() { bootstrap_recovery_emit 'If you expected DNF, check that dnf and rpm are on PATH.' } - ## @fn bootstrap_recovery_unsupported_package_manager() ## @brief Explains next steps when the selected package manager is unsupported. ## @details @@ -88,7 +86,6 @@ bootstrap_recovery_unsupported_package_manager() { bootstrap_recovery_emit 'If this came from configuration, check BOOTSTRAP_PACKAGE_MANAGER in the environment or .env.' } - ## @fn bootstrap_recovery_package_unavailable() ## @brief Explains next steps when a package cannot be found or installed. ## @details @@ -135,7 +132,6 @@ bootstrap_recovery_package_unavailable() { esac } - ## @fn bootstrap_recovery_version_constraint() ## @brief Explains next steps for unsatisfied version constraints. ## @details @@ -176,7 +172,6 @@ bootstrap_recovery_version_constraint() { bootstrap_recovery_emit 'If the available candidate is acceptable, update the manifest constraint.' } - ## @fn bootstrap_recovery_privilege_unavailable() ## @brief Explains next steps when elevated privileges are required but unavailable. ## @details @@ -197,7 +192,6 @@ bootstrap_recovery_privilege_unavailable() { bootstrap_recovery_emit 'Use --dry-run first if you want to inspect the plan before retrying execution.' } - ## @fn bootstrap_recovery_execution_failed() ## @brief Explains next steps when native package installation fails. ## @details diff --git a/src/bootstrap.bash b/src/bootstrap.bash index 3e3efea..5094633 100644 --- a/src/bootstrap.bash +++ b/src/bootstrap.bash @@ -40,7 +40,7 @@ set -euo pipefail bootstrap_print_help() { cat <<'HELP_TEXT' Usage: - bootstrap.bash [options] [manifest] + bootstrap.bash [options] [manifest ...] Options: --help Show this help text and exit. @@ -53,7 +53,7 @@ Options: --quiet Suppress non-essential output. Arguments: - manifest Optional package manifest path. Without --dry-run, resolved actions are executed. + manifest Optional package manifest path. Multiple paths are preflighted before execution. HELP_TEXT } @@ -133,17 +133,30 @@ bootstrap_parse_arguments() { return "${BOOTSTRAP_EXIT_USAGE}" ;; *) - if bootstrap_context_has_manifest_path; then - bootstrap_print_usage_error "unexpected argument: $1" - return "${BOOTSTRAP_EXIT_USAGE}" - fi - bootstrap_context_set_manifest_path "$1" + bootstrap_context_add_manifest_path "$1" ;; esac shift done + local manifest_index + local manifest_path + local stdin_count + + stdin_count=0 + for ((manifest_index = 0; manifest_index < $(bootstrap_context_get_manifest_count); manifest_index++)); do + manifest_path="$(bootstrap_context_get_manifest_path_at "${manifest_index}")" + if [[ "${manifest_path}" == "-" ]]; then + stdin_count=$((stdin_count + 1)) + fi + done + + if ((stdin_count > 1)); then + bootstrap_print_usage_error "standard input manifest '-' may be specified at most once" + return "${BOOTSTRAP_EXIT_USAGE}" + fi + if bootstrap_context_is_verbose && bootstrap_context_is_quiet; then bootstrap_print_usage_error "--verbose and --quiet cannot be used together" return "${BOOTSTRAP_EXIT_USAGE}" @@ -377,14 +390,43 @@ bootstrap_print_resolved_action_explanation() { esac } +## @fn bootstrap_print_manifest_scope() +## @brief Prints the ordered manifests included in the current invocation. +## @details +## Single-manifest output retains the established wording for compatibility. +## Multi-manifest output lists every path explicitly so users can verify the +## preflight scope and correlate later provenance-bearing diagnostics. +## @param heading Singular heading prefix used before the manifest description. +## @retval 0 The manifest scope was printed successfully. +bootstrap_print_manifest_scope() { + local count + local heading + local index + local manifest_path + + heading="$1" + count="$(bootstrap_context_get_manifest_count)" + + if ((count == 1)); then + manifest_path="$(bootstrap_context_get_manifest_path_at 0)" + printf '%s manifest: %s\n' "${heading}" "${manifest_path}" + return "${BOOTSTRAP_EXIT_SUCCESS}" + fi + + printf '%s manifests:\n' "${heading}" + for ((index = 0; index < count; index++)); do + manifest_path="$(bootstrap_context_get_manifest_path_at "${index}")" + printf ' - %s\n' "${manifest_path}" + done +} + ## @fn bootstrap_print_dry_run_plan() -## @brief Renders a planned and resolved dry-run action list for a manifest. +## @brief Renders a planned and resolved dry-run action list for all manifests. ## @details ## Dry-run output is deliberately generated from Action Records and Resolved ## Actions rather than parser records. This keeps user-facing output aligned with ## the same pipeline that later executor phases consume while guaranteeing that ## dry-run mode performs no system changes. -## @param manifest_path Manifest path used to produce the plan. ## @param action_file File containing pipe-delimited Action Records. ## @param resolved_file File containing pipe-delimited Resolved Actions. ## @par Standard Output @@ -394,7 +436,7 @@ bootstrap_print_resolved_action_explanation() { ## @retval 69 A Resolved Action could not be rendered. ## @par Examples ## @code -## bootstrap_print_dry_run_plan packages.txt /tmp/bootstrap-plan.txt /tmp/bootstrap-resolved.txt +## bootstrap_print_dry_run_plan /tmp/bootstrap-plan.txt /tmp/bootstrap-resolved.txt ## @endcode bootstrap_print_dry_run_plan() { local action @@ -404,21 +446,19 @@ bootstrap_print_dry_run_plan() { local operator local package local planned_count - local manifest_path local package_manager_selector local resolved_file local resolved_count local source local version - manifest_path="$1" - action_file="$2" - resolved_file="$3" + action_file="$1" + resolved_file="$2" package_manager_selector="$(bootstrap_context_get_package_manager)" planned_count=0 resolved_count=0 - printf 'Dry run plan for manifest: %s\n' "${manifest_path}" + bootstrap_print_manifest_scope "Dry run plan for" printf '\nPlanned actions:\n' while IFS='|' read -r action package operator version source line_number || [[ -n "${action:-}" ]]; do @@ -455,11 +495,11 @@ bootstrap_print_dry_run_plan() { if bootstrap_context_should_explain; then printf '\nExplanation:\n' - printf ' What happened: bootstrap inspected the manifest, planned package work,\n' + printf ' What happened: bootstrap inspected all manifests, planned package work,\n' printf ' and resolved that plan for the selected package manager.\n' printf ' Safety boundary: --dry-run is active, so execution stops here and no\n' printf ' system changes were made.\n' - printf ' Manifest: %s\n' "${manifest_path}" + bootstrap_print_manifest_scope " Preflight scope includes" printf ' Package manager selector: %s\n' "${package_manager_selector}" printf ' Planned actions: %s\n' "${planned_count}" printf ' Resolved actions: %s\n' "${resolved_count}" @@ -497,40 +537,72 @@ bootstrap_print_dry_run_plan() { fi } -## @fn bootstrap_run_dry_run_plan() -## @brief Parses, plans, resolves, and prints a read-only dry-run plan. +## @fn bootstrap_preflight_manifests() +## @brief Parses, plans, and resolves every supplied manifest before execution. ## @details -## This path composes the manifest parser, planner, and resolver, captures their -## intermediate records, and renders the result without executing resolved -## actions. Temporary files are removed before the function returns. -## @retval 0 The manifest was parsed, planned, resolved, and displayed successfully. -## @retval 65 The manifest could not be parsed or planned. -## @retval 69 The planned actions could not be resolved on this system. -## @par Examples -## @code -## bootstrap_context_set_manifest_path packages.txt -## bootstrap_context_enable_dry_run -## bootstrap_run_dry_run_plan -## @endcode -bootstrap_run_dry_run_plan() { - local action_file +## Each manifest is planned into a private temporary file first. Its Action +## Records are appended to the invocation plan only after that manifest succeeds, +## preventing partial records from a failed manifest from entering the executable +## plan. Resolution runs only after every manifest has parsed and planned +## successfully. This establishes the ADR-049 global preflight barrier. +## +## @param action_file Destination for the complete ordered Action Record stream. +## @param resolved_file Destination for the complete ordered Resolved Action stream. +## @retval 0 Every manifest parsed, planned, and resolved successfully. +## @retval 65 A manifest could not be read, parsed, or planned. +## @retval 69 At least one planned action could not be resolved. +bootstrap_preflight_manifests() { + local count + local index + local manifest_action_file local manifest_path - local resolved_file local status - manifest_path="$(bootstrap_context_get_manifest_path)" - action_file="$(mktemp "${TMPDIR:-/tmp}/bootstrap-plan.XXXXXX")" - resolved_file="$(mktemp "${TMPDIR:-/tmp}/bootstrap-resolved.XXXXXX")" + count="$(bootstrap_context_get_manifest_count)" + : >"$1" + : >"$2" - if bootstrap_planner_plan_manifest_file "${manifest_path}" >"${action_file}"; then - : + for ((index = 0; index < count; index++)); do + manifest_path="$(bootstrap_context_get_manifest_path_at "${index}")" + manifest_action_file="$(mktemp "${TMPDIR:-/tmp}/bootstrap-manifest-plan.XXXXXX")" + + if bootstrap_planner_plan_manifest_file "${manifest_path}" >"${manifest_action_file}"; then + cat "${manifest_action_file}" >>"$1" + rm -f "${manifest_action_file}" + else + status="$?" + rm -f "${manifest_action_file}" + return "${status}" + fi + done + + if bootstrap_resolver_resolve_action_records \ + "$(bootstrap_context_get_package_manager)" <"$1" >"$2"; then + return "${BOOTSTRAP_EXIT_SUCCESS}" else status="$?" - rm -f "${action_file}" "${resolved_file}" return "${status}" fi +} - if bootstrap_resolver_resolve_action_records "$(bootstrap_context_get_package_manager)" <"${action_file}" >"${resolved_file}"; then +## @fn bootstrap_run_dry_run_plan() +## @brief Preflights all manifests and prints one read-only invocation plan. +## @details +## Dry-run uses the same complete Action Record and Resolved Action streams that +## execution would consume. No output plan is rendered until every manifest has +## crossed the global preflight barrier successfully. +## @retval 0 All manifests were preflighted and displayed successfully. +## @retval 65 A manifest could not be parsed or planned. +## @retval 69 The complete action set could not be resolved on this system. +bootstrap_run_dry_run_plan() { + local action_file + local resolved_file + local status + + action_file="$(mktemp "${TMPDIR:-/tmp}/bootstrap-plan.XXXXXX")" + resolved_file="$(mktemp "${TMPDIR:-/tmp}/bootstrap-resolved.XXXXXX")" + + if bootstrap_preflight_manifests "${action_file}" "${resolved_file}"; then : else status="$?" @@ -538,13 +610,13 @@ bootstrap_run_dry_run_plan() { return "${status}" fi - if bootstrap_print_dry_run_plan "${manifest_path}" "${action_file}" "${resolved_file}"; then + if bootstrap_print_dry_run_plan "${action_file}" "${resolved_file}"; then status="${BOOTSTRAP_EXIT_SUCCESS}" else status="$?" fi - rm -f "${action_file}" "${resolved_file}" + rm -f "${action_file}" "${resolved_file}" return "${status}" } @@ -700,41 +772,27 @@ bootstrap_print_execution_results() { } ## @fn bootstrap_run_execution_plan() -## @brief Parses, plans, resolves, executes, and prints execution results. +## @brief Preflights all manifests before executing the complete resolved plan. ## @details -## This path uses the same parser, planner, and resolver pipeline as dry-run -## mode, then streams Resolved Actions into the executor. Dry-run mode stops -## before this function so inspection and execution remain separate. -## @retval 0 The manifest was executed successfully or no actions were needed. -## @retval 65 The manifest could not be parsed or planned. -## @retval 69 The planned actions could not be resolved on this system. +## No executor is invoked until every supplied manifest has parsed, planned, and +## resolved successfully. The resulting Resolved Action stream preserves manifest +## argument order and source provenance, satisfying ADR-049 without concatenating +## source files or losing filename and line-number diagnostics. +## @retval 0 The complete manifest set executed successfully or required no work. +## @retval 65 At least one manifest could not be parsed or planned. +## @retval 69 At least one planned action could not be resolved. ## @retval 70 At least one resolved action failed during execution. -## @par Examples -## @code -## bootstrap_context_set_manifest_path packages.txt -## bootstrap_run_execution_plan -## @endcode bootstrap_run_execution_plan() { local action_file - local manifest_path local resolved_file local result_file local status - manifest_path="$(bootstrap_context_get_manifest_path)" action_file="$(mktemp "${TMPDIR:-/tmp}/bootstrap-plan.XXXXXX")" resolved_file="$(mktemp "${TMPDIR:-/tmp}/bootstrap-resolved.XXXXXX")" result_file="$(mktemp "${TMPDIR:-/tmp}/bootstrap-results.XXXXXX")" - if bootstrap_planner_plan_manifest_file "${manifest_path}" >"${action_file}"; then - : - else - status="$?" - rm -f "${action_file}" "${resolved_file}" "${result_file}" - return "${status}" - fi - - if bootstrap_resolver_resolve_action_records "$(bootstrap_context_get_package_manager)" <"${action_file}" >"${resolved_file}"; then + if bootstrap_preflight_manifests "${action_file}" "${resolved_file}"; then : else status="$?" @@ -802,7 +860,7 @@ main() { bootstrap_parse_arguments "$@" || return "$?" bootstrap_config_validate_effective_runtime || return "$?" - if bootstrap_context_has_manifest_path; then + if bootstrap_context_has_manifest_paths; then if bootstrap_context_is_dry_run; then bootstrap_run_dry_run_plan return "$?" diff --git a/tests/cli-dry-run.bats b/tests/cli-dry-run.bats index a82edaa..11b1a5a 100644 --- a/tests/cli-dry-run.bats +++ b/tests/cli-dry-run.bats @@ -53,7 +53,7 @@ MANIFEST [ "$status" -eq 0 ] [[ "$output" == *"Explanation:"* ]] - [[ "$output" == *"What happened: bootstrap inspected the manifest"* ]] + [[ "$output" == *"What happened: bootstrap inspected all manifests"* ]] [[ "$output" == *"Safety boundary: --dry-run is active"* ]] [[ "$output" == *"Package manager selector: auto"* ]] [[ "$output" == *"How to read this output:"* ]] @@ -107,14 +107,39 @@ MANIFEST [[ "$output" == *"install package: make"* ]] } -@test "dry-run rejects multiple manifest arguments" { +@test "dry-run preflights multiple manifests in argument order" { first="${WORK_DIR}/first.txt" second="${WORK_DIR}/second.txt" printf 'git\n' >"$first" printf 'curl\n' >"$second" + run "$SCRIPT" --dry-run --explain "$first" "$second" + + [ "$status" -eq 0 ] + [[ "$output" == *"Dry run plan for manifests:"* ]] + [[ "$output" == *" - $first"* ]] + [[ "$output" == *" - $second"* ]] + [[ "$output" == *"$first:1 requested package git"* ]] + [[ "$output" == *"$second:1 requested package curl"* ]] + [[ "$output" == *"Summary: 2 action(s) planned; 2 action(s) resolved."* ]] +} + +@test "dry-run reports the original filename and line for a later manifest error" { + first="${WORK_DIR}/valid.txt" + second="${WORK_DIR}/invalid.txt" + printf 'git\n' >"$first" + printf 'curl\ninvalid package entry\n' >"$second" + run "$SCRIPT" --dry-run "$first" "$second" + [ "$status" -eq 65 ] + [[ "$output" == *"location: $second:2"* ]] + [[ "$output" != *"Dry run plan for manifests:"* ]] +} + +@test "command line rejects more than one standard input manifest" { + run "$SCRIPT" --dry-run - - + [ "$status" -eq 64 ] - [[ "$output" == *"unexpected argument: $second"* ]] + [[ "$output" == *"standard input manifest '-' may be specified at most once"* ]] } diff --git a/tests/cli-integration.bats b/tests/cli-integration.bats index 67078a9..4582c03 100644 --- a/tests/cli-integration.bats +++ b/tests/cli-integration.bats @@ -123,3 +123,39 @@ STUB [[ "$output" == *"total: 0"* ]] [ ! -e "$mutation_log" ] } + +@test "a later parse failure prevents execution of every manifest" { + write_resolver_stubs + first="${WORK_DIR}/first-valid.txt" + second="${WORK_DIR}/second-invalid.txt" + mutation_log="${WORK_DIR}/preflight-parse-mutation.log" + printf 'git\n' >"$first" + printf 'not valid syntax\n' >"$second" + + run env \ + PATH="${FAKE_BIN}:$PATH" \ + PHASE9_MUTATION_LOG="$mutation_log" \ + "$SCRIPT" --package-manager apt "$first" "$second" + + [ "$status" -eq 65 ] + [[ "$output" == *"location: $second:1"* ]] + [ ! -e "$mutation_log" ] +} + +@test "a later resolution failure prevents execution of every manifest" { + write_resolver_stubs + first="${WORK_DIR}/first-resolvable.txt" + second="${WORK_DIR}/second-unresolvable.txt" + mutation_log="${WORK_DIR}/preflight-resolve-mutation.log" + printf 'git\n' >"$first" + printf 'missing-package\n' >"$second" + + run env \ + PATH="${FAKE_BIN}:$PATH" \ + PHASE9_MUTATION_LOG="$mutation_log" \ + "$SCRIPT" --package-manager apt "$first" "$second" + + [ "$status" -eq 69 ] + [[ "$output" == *"apt package not available: missing-package"* ]] + [ ! -e "$mutation_log" ] +} diff --git a/tests/trivial.bats b/tests/trivial.bats index cf790e1..98302e7 100644 --- a/tests/trivial.bats +++ b/tests/trivial.bats @@ -44,7 +44,7 @@ setup() { [ "$status" -eq 0 ] [[ "$output" == *"Usage:"* ]] - [[ "$output" == *"bootstrap.bash [options] [manifest]"* ]] + [[ "$output" == *"bootstrap.bash [options] [manifest ...]"* ]] [[ "$output" == *"--dry-run"* ]] [[ "$output" == *"--explain"* ]] [[ "$output" == *"--verbose"* ]]