valise-sh 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,63 +3,29 @@
3
3
  # Copyright (c) 2026 Joseph Cumines
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- # build-manifest.sh: vendor engine files into the e2e release tree and
6
+ # build-manifest.sh: vendor engine files into an e2e release tree and
7
7
  # generate a profile-release.manifest with correct sizes and SHA-256 digests.
8
8
  #
9
- # Usage: run from any directory. The script resolves paths relative to itself.
10
- # Output: shell/install/profile-release.manifest (overwritten each run).
9
+ # Usage: sh build-manifest.sh [output-dir]
10
+ # Default output-dir: examples/e2e/build/release
11
11
 
12
12
  set -eu
13
13
 
14
14
  _script_dir=$(cd "$(dirname "$0")" && pwd -P)
15
15
  _repo_root=$(cd "$_script_dir/../.." && pwd -P)
16
16
  _e2e_root="$_script_dir"
17
+ _out_dir="${1:-$_e2e_root/build/release}"
18
+ _valise="$_repo_root/bin/valise"
17
19
 
18
- # --- Locate SHA-256 utility ---
19
- # Match bin/valise fallback chain: sha256sum, shasum -a 256, sha256 (BSD).
20
- _sha_cmd=""
21
- if command -v sha256sum >/dev/null 2>&1; then
22
- _sha_cmd="sha256sum"
23
- elif command -v shasum >/dev/null 2>&1; then
24
- _sha_cmd="shasum"
25
- elif command -v sha256 >/dev/null 2>&1; then
26
- _sha_cmd="sha256"
27
- else
28
- printf 'ERROR: no SHA-256 utility found (need sha256sum, shasum, or sha256)\n' >&2
29
- exit 1
30
- fi
31
-
32
- file_sha256() {
33
- _sha_output=""
34
- if [ "$_sha_cmd" = "sha256sum" ]; then
35
- _sha_output=$(sha256sum "$1")
36
- elif [ "$_sha_cmd" = "shasum" ]; then
37
- _sha_output=$(shasum -a 256 "$1")
38
- else
39
- _sha_output=$(sha256 "$1")
40
- fi
41
- # BSD sha256 prints "SHA256 (filename) = digest"; sha256sum/shasum print
42
- # "digest filename". Gate the BSD strip behind the SHA256 ( prefix to
43
- # avoid corrupting output when the filename contains "= ".
44
- case "$_sha_output" in
45
- "SHA256 ("*) _sha_output=${_sha_output##* = } ;;
46
- esac
47
- _sha_output=${_sha_output%% *}
48
- _sha_output=${_sha_output%%$(printf '\t')*}
49
- printf '%s\n' "$_sha_output"
50
- }
51
-
52
- file_bytes() {
53
- wc -c < "$1" | tr -d '[:space:]'
54
- }
20
+ [ -x "$_valise" ] || { printf 'ERROR: valise CLI not found at %s\n' "$_valise" >&2; exit 1; }
55
21
 
56
- # --- Vendor engine files ---
57
- # These are copies of the engine source that form the release payload.
58
- # They are NOT committed (see .gitignore); this script regenerates them.
22
+ # Prepare clean output directory
23
+ rm -rf "$_out_dir"
24
+ mkdir -p "$_out_dir"
59
25
 
60
26
  _vendor_copy() {
61
27
  _src="$_repo_root/$1"
62
- _dst="$_e2e_root/$1"
28
+ _dst="$_out_dir/$1"
63
29
  _dst_dir=$(dirname "$_dst")
64
30
  [ -f "$_src" ] || { printf 'ERROR: engine source missing: %s\n' "$_src" >&2; exit 1; }
65
31
  mkdir -p "$_dst_dir"
@@ -67,7 +33,7 @@ _vendor_copy() {
67
33
  chmod 0644 "$_dst"
68
34
  }
69
35
 
70
- printf 'Vendoring engine files from %s...\n' "$_repo_root"
36
+ printf 'Vendoring engine files into %s...\n' "$_out_dir"
71
37
 
72
38
  # Core libraries
73
39
  _vendor_copy lib/metadata.sh
@@ -79,8 +45,7 @@ _vendor_copy lib/compiler.awk
79
45
  _vendor_copy lib/fish/loader.fish
80
46
  _vendor_copy lib/fish/metadata.fish
81
47
 
82
- # Generic Fish entrypoint adapter (sourced by the generated Fish
83
- # integration after it authenticates the active release)
48
+ # Generic Fish entrypoint adapter
84
49
  _vendor_copy entrypoints/fish/profile.fish
85
50
 
86
51
  # Entrypoint
@@ -115,14 +80,37 @@ if [ "$_engine_contrib_count" -eq 0 ]; then
115
80
  exit 1
116
81
  fi
117
82
 
118
- # --- Vendor shared layer (cross-repo composition) ---
119
- # The provider tree at examples/shared simulates a version-pinned external
120
- # repository. Its files are copied under layer/shared/ so the release keeps
121
- # provider bytes separate from consumer bytes while one manifest covers the
122
- # whole closure. layer-pin.txt records the pinned provider revision.
83
+ # --- Native e2e example modules ---
84
+ _e2e_meta_count=0
85
+ mkdir -p "$_out_dir/module.d"
86
+ for _meta in "$_e2e_root"/module.d/e2e_*.meta; do
87
+ [ -f "$_meta" ] || continue
88
+ cp "$_meta" "$_out_dir/module.d/"
89
+ chmod 0644 "$_out_dir/module.d/$(basename "$_meta")"
90
+ _e2e_meta_count=$((_e2e_meta_count + 1))
91
+ done
92
+
93
+ _e2e_body_count=0
94
+ mkdir -p "$_out_dir/modules/e2e"
95
+ for _body in "$_e2e_root"/modules/e2e/*.sh "$_e2e_root"/modules/e2e/*.fish; do
96
+ [ -f "$_body" ] || continue
97
+ cp "$_body" "$_out_dir/modules/e2e/"
98
+ chmod 0644 "$_out_dir/modules/e2e/$(basename "$_body")"
99
+ _e2e_body_count=$((_e2e_body_count + 1))
100
+ done
123
101
 
102
+ if [ "$_e2e_meta_count" -eq 0 ]; then
103
+ printf 'ERROR: no e2e module metadata found in %s\n' "$_e2e_root/module.d" >&2
104
+ exit 1
105
+ fi
106
+ if [ "$_e2e_body_count" -eq 0 ]; then
107
+ printf 'ERROR: no e2e module bodies found in %s\n' "$_e2e_root/modules/e2e" >&2
108
+ exit 1
109
+ fi
110
+
111
+ # --- Vendor shared layer (cross-repo composition) ---
124
112
  _shared_src="$_repo_root/examples/shared"
125
- _shared_dst="$_e2e_root/layer/shared"
113
+ _shared_dst="$_out_dir/layer/shared"
126
114
  [ -d "$_shared_src/module.d" ] || { printf 'ERROR: shared provider missing: %s\n' "$_shared_src" >&2; exit 1; }
127
115
  mkdir -p "$_shared_dst/module.d" "$_shared_dst/modules"
128
116
  for _shared_meta in "$_shared_src"/module.d/*.meta; do
@@ -142,109 +130,9 @@ done
142
130
  _shared_rev=$(cd "$_shared_src" && git rev-parse HEAD 2>/dev/null || printf 'unknown')
143
131
  printf 'provider=examples/shared\nrevision=%s\n' "$_shared_rev" > "$_shared_dst/layer-pin.txt"
144
132
  chmod 0644 "$_shared_dst/layer-pin.txt"
145
- printf 'Vendored shared layer from %s (revision %s)\n' "$_shared_src" "$_shared_rev"
146
-
147
- # --- Generate manifest ---
148
- _manifest_dir="$_e2e_root/shell/install"
149
- mkdir -p "$_manifest_dir"
150
- _manifest="$_manifest_dir/profile-release.manifest"
151
-
152
- printf 'Generating manifest...\n'
153
-
154
- # Collect all files to include in the manifest.
155
- # Engine files (vendored) + e2e example modules (native).
156
- _file_list=""
157
-
158
- # Engine files
159
- for _f in \
160
- lib/metadata.sh lib/loader.sh lib/capabilities.sh lib/compiler.awk \
161
- lib/fish/loader.fish lib/fish/metadata.fish \
162
- entrypoints/profile.sh entrypoints/fish/profile.fish \
163
- module.d/platform_posix.meta module.d/platform_macos.meta \
164
- module.d/platform_linux.meta module.d/platform_bsd.meta \
165
- modules/platform/posix.sh \
166
- modules/platform/macos/capabilities.sh \
167
- modules/platform/linux/capabilities.sh \
168
- modules/platform/bsd/capabilities.sh \
169
- ; do
170
- _file_list="$_file_list $_f"
171
- done
172
133
 
173
- # E2e example module metadata
174
- _e2e_meta_count=0
175
-
176
- # Shareable engine catalog (vendored above)
177
- for _f in "$_e2e_root"/module.d/contrib_*.meta "$_e2e_root"/modules/system/contrib_*.sh; do
178
- [ -f "$_f" ] || continue
179
- _rel=${_f#"$_e2e_root"/}
180
- _file_list="$_file_list $_rel"
181
- done
182
- for _f in "$_e2e_root"/module.d/e2e_*.meta; do
183
- [ -f "$_f" ] || continue
184
- _rel=${_f#"$_e2e_root"/}
185
- _file_list="$_file_list $_rel"
186
- _e2e_meta_count=$((_e2e_meta_count + 1))
187
- done
188
-
189
- # E2e example module bodies (POSIX)
190
- _e2e_sh_count=0
191
- for _f in "$_e2e_root"/modules/e2e/*.sh; do
192
- [ -f "$_f" ] || continue
193
- _rel=${_f#"$_e2e_root"/}
194
- _file_list="$_file_list $_rel"
195
- _e2e_sh_count=$((_e2e_sh_count + 1))
196
- done
197
-
198
- # E2e example module bodies (Fish)
199
- _e2e_fish_count=0
200
- for _f in "$_e2e_root"/modules/e2e/*.fish; do
201
- [ -f "$_f" ] || continue
202
- _rel=${_f#"$_e2e_root"/}
203
- _file_list="$_file_list $_rel"
204
- _e2e_fish_count=$((_e2e_fish_count + 1))
205
- done
206
-
207
- # Shared layer files (cross-repo composition): provider metadata, bodies,
208
- # and the pin file recording the vendored revision.
209
- _shared_count=0
210
- for _f in "$_e2e_root"/layer/shared/module.d/*.meta "$_e2e_root"/layer/shared/modules/*/*.sh "$_e2e_root"/layer/shared/layer-pin.txt; do
211
- [ -f "$_f" ] || continue
212
- _rel=${_f#"$_e2e_root"/}
213
- _file_list="$_file_list $_rel"
214
- _shared_count=$((_shared_count + 1))
215
- done
216
-
217
- # Fail closed when a glob collected nothing: POSIX sh leaves a non-matching
218
- # glob unexpanded, so without this an empty or missing e2e directory would
219
- # build a manifest silently missing e2e files.
220
- if [ "$_e2e_meta_count" -eq 0 ]; then
221
- printf 'ERROR: no e2e module metadata found in %s\n' "$_e2e_root/module.d" >&2
222
- exit 1
223
- fi
224
- if [ "$((_e2e_sh_count + _e2e_fish_count))" -eq 0 ]; then
225
- printf 'ERROR: no e2e module bodies found in %s\n' "$_e2e_root/modules/e2e" >&2
226
- exit 1
227
- fi
228
- if [ "$_shared_count" -eq 0 ]; then
229
- printf 'ERROR: no shared layer files found in %s\n' "$_e2e_root/layer/shared" >&2
230
- exit 1
231
- fi
232
-
233
- # Write manifest header
234
- printf 'shell-profile-release-manifest-v1\n' > "$_manifest"
235
- printf 'destination\tsource\tsource_mode\tinstall_mode\tbytes\tsha256\n' >> "$_manifest"
236
-
237
- # Write data rows
238
- for _rel in $_file_list; do
239
- _full="$_e2e_root/$_rel"
240
- [ -f "$_full" ] || { printf 'ERROR: file missing: %s\n' "$_full" >&2; exit 1; }
241
- [ ! -L "$_full" ] || { printf 'ERROR: symlink in release: %s\n' "$_full" >&2; exit 1; }
242
- _bytes=$(file_bytes "$_full")
243
- _sha=$(file_sha256 "$_full")
244
- # destination == source for this flat release layout
245
- # All release files are 0644 source -> 0444 installed (immutable)
246
- printf '%s\t%s\t0644\t0444\t%s\t%s\n' "$_rel" "$_rel" "$_bytes" "$_sha" >> "$_manifest"
247
- done
134
+ # Normalise file modes before manifesting
135
+ find "$_out_dir" -type f -exec chmod 0644 {} +
248
136
 
249
- _file_count=$(awk 'NR > 2 && NF > 0 { count++ } END { print count+0 }' "$_manifest")
250
- printf 'Manifest written: %s (%d files)\n' "$_manifest" "$_file_count"
137
+ # --- Generate manifest using valise CLI ---
138
+ "$_valise" make-manifest "$_out_dir"
@@ -0,0 +1,181 @@
1
+ # profile-project: a complete valise project
2
+
3
+ This is a small but complete consuming project. It shows every piece a
4
+ profile project needs: metadata that describes the modules, module bodies,
5
+ a provider layer contributed by somebody else, a bundle that says which
6
+ roots load, a variant that says what to leave out, and project defaults.
7
+
8
+ Everything here is plain text. There is no build script and no hand-written
9
+ registry. `valise` reads the metadata and generates what it needs.
10
+
11
+ ## What is in it
12
+
13
+ ```
14
+ shell/
15
+ module.d/ the project's own modules
16
+ profile_main.meta the entry module
17
+ greeting.meta a dependency of profile_main
18
+ modules/ the bodies of those modules
19
+ greeting.sh
20
+ layers/demo/ a provider layer
21
+ module.d/contrib_demo_greeting.meta
22
+ modules/shared/contrib_demo_greeting.sh
23
+ release/
24
+ bundle-roots which roots load, per shell
25
+ install-variants named exclusion sets
26
+ project.conf defaults for build and apply
27
+ ```
28
+
29
+ `profile_main` is a meta module: it has `requires=greeting` and no body of
30
+ its own, so there is no `modules/profile_main.sh`. It exists only to name
31
+ what the project loads. `greeting` is an ordinary module that defines
32
+ `greet`.
33
+
34
+ The layer under `shell/layers/demo/` is a provider layer. Its module is
35
+ named `contrib_demo_greeting`, following the `contrib_<owner>_<thing>`
36
+ convention, and its body lives under `layers/demo/` in the installed
37
+ release, so it never collides with the project's own modules.
38
+
39
+ ## Build it
40
+
41
+ From this directory:
42
+
43
+ ```sh
44
+ valise build
45
+ ```
46
+
47
+ That compiles a plan for each supported shell and writes a release under
48
+ `build/release/`. The release contains:
49
+
50
+ ```
51
+ build/release/
52
+ generated/installed-modules.bash compiled plan for bash
53
+ generated/installed-modules.zsh compiled plan for zsh
54
+ generated/installed-modules.ksh compiled plan for ksh
55
+ vendor/shell-profile/lib/capabilities.sh
56
+ module.d/ the project's metadata
57
+ modules/ the project's bodies
58
+ layers/demo/module.d/ the layer's metadata
59
+ layers/demo/modules/shared/ the layer's bodies
60
+ shell/release/ registry, bundle, variants, defaults
61
+ shell/install/profile-release.manifest
62
+ ```
63
+
64
+ The plans are plain POSIX shell. You can read one to see exactly what will
65
+ run.
66
+
67
+ ## Load it
68
+
69
+ ```sh
70
+ valise apply
71
+ ```
72
+
73
+ `apply` builds the release, installs it under your XDG data directory, and
74
+ writes a managed block into the startup files your shells read. It prints
75
+ what it changed and where.
76
+
77
+ The managed block is guarded by a source-once variable, so your login shell
78
+ loads the profile exactly once. It prefers the compiled plan for your shell
79
+ and falls back to the loader if the release has no compiled plan.
80
+
81
+ zsh does not read `~/.profile` on its own, so when zsh is present `apply`
82
+ writes the same managed block to `~/.zprofile`. bash reads `~/.profile` only
83
+ when no `~/.bash_profile` exists; `apply` never creates one, so it does not
84
+ shadow your `~/.profile`.
85
+
86
+ Now open a new login shell:
87
+
88
+ ```sh
89
+ bash -l
90
+ greet
91
+ # hello from the valise profile project
92
+ ```
93
+
94
+ The same works in zsh and ksh. It does not work in dash: the plans are
95
+ compiled for bash, zsh and ksh, and dash is not one of them.
96
+
97
+ To undo it, remove the managed block from every startup file `apply`
98
+ reported.
99
+
100
+ To check the current state:
101
+
102
+ ```sh
103
+ valise status
104
+ ```
105
+
106
+ ```
107
+ project: /path/to/profile-project
108
+ variant: default
109
+ bundle: default
110
+ integration: /home/you/.profile: managed block present
111
+ integration: /home/you/.zprofile: managed block present
112
+ registry: up to date
113
+ active release: 0a723dcb0860bcae3ea259c631aaa76ce8e0b4e6991448979018c1d08fc35802
114
+ build output: /path/to/profile-project/build/release
115
+ drift: active release matches build output
116
+ ```
117
+
118
+ `status` reports the active release, the startup files, and whether the
119
+ installed release still matches what the project would build. If you edit a
120
+ module and re-apply, the release changes and the drift line tells you the
121
+ active release is no longer the one you built.
122
+
123
+ ## Try a variant
124
+
125
+ The project ships two variants. `default` loads everything;
126
+ `workstation-lean` leaves out the provider layer:
127
+
128
+ ```sh
129
+ valise explain-variant workstation-lean
130
+ ```
131
+
132
+ To use the lean variant, set it in `shell/release/project.conf`:
133
+
134
+ ```sh
135
+ variant=workstation-lean
136
+ ```
137
+
138
+ Then re-apply. The `contrib_demo_greeting` module disappears from the plan
139
+ and the rest still loads.
140
+
141
+ An exclusion that removes a module something else still depends on is an
142
+ error, not a silent surprise:
143
+
144
+ ```
145
+ ERROR: variant exclusion breaks a required dependency: profile_main requires contrib_demo_greeting
146
+ ```
147
+
148
+ ## Try a bundle
149
+
150
+ `shell/release/bundle-roots` says which roots load for each shell:
151
+
152
+ ```
153
+ default bash profile_main contrib_demo_greeting@shell/layers/demo/module.d
154
+ ```
155
+
156
+ The first root is a bare name, meaning a module from the project's own
157
+ `shell/module.d/`. The second is qualified with its source, so it is
158
+ unambiguous even though it lives in a layer.
159
+
160
+ ```sh
161
+ valise explain-bundles
162
+ ```
163
+
164
+ ## Check the layer
165
+
166
+ A provider layer has rules: the `contrib_` prefix, a version, a body that
167
+ exists, and no dependency that escapes the layer.
168
+
169
+ ```sh
170
+ valise layer-lint demo
171
+ # layer demo: 1 module(s) validated
172
+ ```
173
+
174
+ ## Where to go next
175
+
176
+ - [The module ABI](../../docs/module-abi.md) — every metadata field.
177
+ - [The authoring guide](../../docs/contrib-modules.md) — writing modules.
178
+ - [The compile and build contract](../../docs/variants.md) — the registry,
179
+ bundles, variants, and what a compiled plan contains.
180
+ - [Shared modules](../../docs/shared-modules.md) — how layer modules are
181
+ named and owned.
@@ -0,0 +1,10 @@
1
+ name=contrib_demo_greeting
2
+ version=1.0.0
3
+ side_effect=read-only
4
+ requires=
5
+ optional=
6
+ shells=sh bash zsh ksh
7
+ platforms=any
8
+ exports=contrib_demo_greeting_ready
9
+ updates=
10
+ load=modules/shared/contrib_demo_greeting.sh
@@ -0,0 +1,4 @@
1
+ # A provider layer module. Layer-only modules must carry the `contrib_`
2
+ # prefix; `valise layer-lint demo` enforces that, along with the version,
3
+ # the load target, the file modes, and the dependency closure.
4
+ contrib_demo_greeting_ready() { :; }
@@ -0,0 +1,10 @@
1
+ name=greeting
2
+ version=1.0.0
3
+ side_effect=read-only
4
+ requires=
5
+ optional=
6
+ shells=sh bash zsh ksh
7
+ platforms=any
8
+ exports=greet
9
+ updates=
10
+ load=modules/greeting.sh
@@ -0,0 +1,9 @@
1
+ name=profile_main
2
+ version=1.0.0
3
+ side_effect=read-only
4
+ requires=greeting
5
+ optional=
6
+ shells=bash zsh ksh
7
+ platforms=any
8
+ exports=
9
+ updates=
@@ -0,0 +1,5 @@
1
+ # A plain POSIX module. `shells=sh` covers every POSIX shell: sh, bash,
2
+ # zsh, ksh, and dash.
3
+ greet() {
4
+ printf '%s\n' "hello from the valise profile project"
5
+ }
@@ -0,0 +1,9 @@
1
+ # bundle shell roots
2
+ # The first row is a header, not a bundle definition. Each row names a
3
+ # bundle, a shell, and the space-separated root modules for that shell.
4
+ #
5
+ # A layer module is named by its qualified identity so the source directory
6
+ # is unambiguous: name@shell/layers/<provider>/module.d
7
+ default bash profile_main contrib_demo_greeting@shell/layers/demo/module.d
8
+ default zsh profile_main contrib_demo_greeting@shell/layers/demo/module.d
9
+ default ksh profile_main contrib_demo_greeting@shell/layers/demo/module.d
@@ -0,0 +1,3 @@
1
+ variant_name description excluded_modules
2
+ default Every module in the project
3
+ workstation-lean No provider layer contrib_demo_greeting@shell/layers/demo/module.d
@@ -0,0 +1,4 @@
1
+ name version side_effect requires optional shells platforms exports load updates source
2
+ greeting 1.0.0 read-only sh bash zsh ksh any greet modules/greeting.sh shell/module.d
3
+ profile_main 1.0.0 read-only greeting bash zsh ksh any shell/module.d
4
+ contrib_demo_greeting 1.0.0 read-only sh bash zsh ksh any contrib_demo_greeting_ready modules/shared/contrib_demo_greeting.sh shell/layers/demo/module.d
@@ -0,0 +1,3 @@
1
+ # Project defaults. `valise build` and `valise apply` read these.
2
+ variant=default
3
+ bundle=default
@@ -26,9 +26,9 @@ level README).
26
26
 
27
27
  ```sh
28
28
  sh examples/e2e/build-manifest.sh
29
- cd examples/e2e
29
+ cd examples/e2e/build/release
30
30
  valise install
31
- valise verify-active
31
+ valise release verify
32
32
  ```
33
33
 
34
34
  Then load the shared modules from a POSIX shell. The active release path