Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions base_api_manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ artifacts:
- docs/single-file-distribution.md|documentation|single-file and deterministic bundle workflow
- docs/vendor-workflow.md|documentation|offline vendor and standalone workflow
- docs/v2-symbol-map.md|documentation|v2 migration and symbol map
- scripts/standalone-app-payloads.txt|runtime-manifest|shared generated-app and standalone payload allowlist
- lib/bash/README.md|documentation|package layout and namespace contract
- lib/bash/tests/test_helper.sh|test-support|shared BATS helpers

Expand Down
12 changes: 10 additions & 2 deletions bin/base-bash
Original file line number Diff line number Diff line change
Expand Up @@ -619,11 +619,19 @@ base_launcher_init() {
return 1
}

files=(README.md VERSION BASE_BASH_LIBS_PIN .gitignore .editorconfig .shellcheckrc bin/app lib/app.sh config/app.conf.example tests/app.bats)
package_root="$(base_launcher_package_root)" || return 1
[[ -r "$package_root/scripts/standalone-app-payloads.txt" ]] || {
printf 'ERROR: standalone application payload manifest is missing from %s\n' "$package_root" >&2
return 1
}
while IFS= read -r argument || [[ -n "$argument" ]]; do
[[ -n "$argument" && "$argument" != \#* ]] || continue
files+=("$argument")
done < "$package_root/scripts/standalone-app-payloads.txt"
files+=(.gitignore .editorconfig .shellcheckrc tests/app.bats)
if [[ "$profile" == standard ]]; then
files+=(tests/run.sh Makefile .github/workflows/validate.yml tools/shfmt-check.sh)
fi
package_root="$(base_launcher_package_root)" || return 1
__base_bash_libs_launcher_init_resolve_pin__ "$package_root" || return $?
for argument in "${files[@]}"; do
mkdir -p "$target_dir/$(dirname -- "$argument")" || return 1
Expand Down
28 changes: 26 additions & 2 deletions docs/vendor-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,34 @@ For an application that must run without a framework checkout, assemble a
standalone directory:

```bash
scripts/vendor standalone . /tmp/base-bash-libs-v2 dist/app
PATH="$PWD/dist/app/bin:$PATH" dist/app/bin/app --help
scripts/vendor standalone . /tmp/base-bash-libs-v2 /tmp/my-app-dist
PATH="/tmp/my-app-dist/bin:$PATH" /tmp/my-app-dist/bin/app --help
```

The required application payload is defined once in
`scripts/standalone-app-payloads.txt`; both the project generator and standalone
packager use that list. Development-only repository metadata, local
configuration overrides, tests, build output, caches, and previous
output/staging trees are not recursively copied. Put additional runtime assets
under `assets/` or `config/` and name each one explicitly:

```bash
scripts/vendor standalone . /tmp/base-bash-libs-v2 /tmp/my-app-dist \
--include assets/templates/default.conf \
--include config/production.conf
```

Included paths must be regular files with no symlink in any path component.
The packager validates each path before copying, checks the source again around
the copy, and verifies the staged bytes against the pre-copy digest; if a path
changes during staging, packaging fails and the incomplete staging tree is
discarded. These checks apply equally to required files and explicitly selected
runtime assets. The destination parent must already exist, and the destination
must be outside the application source tree; containment is checked using
filesystem identity so alternate path casing on case-insensitive filesystems
cannot bypass it. Use a sibling or temporary output directory. This prevents
an output/staging tree from becoming part of its own package input.

The standalone payload contains two deterministic copies of the same verified
framework bundle. The root copy is the authoritative runtime layout and is
bound by `BASE_BASH_STANDALONE.release`; the launcher resolves its colocated
Expand Down
7 changes: 7 additions & 0 deletions scripts/standalone-app-payloads.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Required generated-app payload files shared by the launcher and vendor tool.
README.md
VERSION
BASE_BASH_LIBS_PIN
bin/app
lib/app.sh
config/app.conf.example
167 changes: 150 additions & 17 deletions scripts/vendor
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Usage:
scripts/vendor update BUNDLE DESTINATION
scripts/vendor rollback DESTINATION
scripts/vendor verify DESTINATION
scripts/vendor standalone APPLICATION BUNDLE DESTINATION
scripts/vendor standalone APPLICATION BUNDLE DESTINATION [--include assets/PATH|config/PATH ...]
EOF
}

Expand Down Expand Up @@ -48,14 +48,87 @@ copy_verified_bundle() {
cp -- "$source/MANIFEST.sha256" "$destination/MANIFEST.sha256" || return 1
}

copy_tree() {
local source="$1" destination="$2" file relative
while IFS= read -r -d '' file; do
relative="${file#"$source/"}"
validate_payload_file() {
local source="$1" relative="$2" candidate component
local -a components=()

[[ -n "$relative" && "$relative" != /* && "$relative" != */ ]] || {
error "application payload path is not a clean relative path: $relative"
return 1
}
[[ "$relative" != *//* ]] || {
error "application payload path contains an empty component: $relative"
return 1
}
case "$relative" in
. | .. | ./* | ../* | */./* | */../* | */. | */.. | *$'\n'* | *$'\r'*)
error "application payload path contains an unsafe component: $relative"
return 1
;;
esac

IFS='/' read -r -a components <<< "$relative"
candidate="$source"
for component in "${components[@]}"; do
candidate="$candidate/$component"
[[ ! -L "$candidate" ]] || {
error "application payload path traverses a symlink: $relative"
return 1
}
done
[[ -f "$candidate" ]] || {
error "application payload entry is not a regular file: $relative"
return 1
}
}

copy_payload_files() {
local source="$1" destination="$2" relative source_hash copied_hash source_executable
shift 2
while IFS= read -r relative; do
[[ -n "$relative" ]] || continue
validate_payload_file "$source" "$relative" || return 1
source_hash="$(hash_file "$source/$relative")" || return 1
validate_payload_file "$source" "$relative" || return 1
source_executable=0
[[ -x "$source/$relative" ]] && source_executable=1
mkdir -p "$destination/$(dirname -- "$relative")" || return 1
cp -- "$file" "$destination/$relative" || return 1
if [[ -x "$file" ]]; then chmod +x "$destination/$relative" || return 1; fi
done < <(find "$source" -type f -print0 | LC_ALL=C sort -z)
cp -P -- "$source/$relative" "$destination/$relative" || return 1
[[ -f "$destination/$relative" && ! -L "$destination/$relative" ]] || {
error "application payload changed to a symlink while it was copied: $relative"
return 1
}
validate_payload_file "$source" "$relative" || return 1
copied_hash="$(hash_file "$destination/$relative")" || return 1
[[ "$copied_hash" == "$source_hash" ]] || {
error "application payload changed while it was copied: $relative"
return 1
}
if ((source_executable)); then chmod +x "$destination/$relative" || return 1; fi
done < <(printf '%s\n' "$@" | LC_ALL=C sort -u)
}

standalone_destination_is_external() {
local source="$1" destination="$2" destination_parent destination_name canonical ancestor
destination_parent="$(cd -- "$(dirname -- "$destination")" 2> /dev/null && pwd -P)" || {
error "standalone destination parent does not exist: $(dirname -- "$destination")"
return 1
}
destination_name="$(basename -- "$destination")"
canonical="$destination_parent/$destination_name"
ancestor="$destination_parent"
while :; do
if [[ "$ancestor" -ef "$source" ]]; then
error "standalone destination must be outside the application source tree: $destination"
return 1
fi
[[ "$ancestor" == / ]] && break
ancestor="$(dirname -- "$ancestor")" || return 1
done
if [[ (-e "$canonical" || -L "$canonical") && "$canonical" -ef "$source" ]]; then
error "standalone destination must be outside the application source tree: $destination"
return 1
fi
}

write_lock() {
Expand Down Expand Up @@ -194,12 +267,59 @@ verify_destination() {
}

standalone_bundle() {
local application="$1" framework_bundle="$2" destination="$3" temporary
local application="$1" framework_bundle="$2" destination="$3" temporary application_payload relative
local application_root required_payload
local -a payload_files=() selected_paths=()
local -A payload_seen=() required_payloads=()
shift 3
while (($#)); do
case "$1" in
--include)
(($# >= 2)) || {
error "--include requires a relative path under assets/ or config/"
return 2
}
payload_files+=("$2")
shift 2
;;
*)
error "unknown standalone option: $1"
return 2
;;
esac
done

verify_bundle "$framework_bundle" || return $?
[[ -d "$application" && -f "$application/bin/app" ]] || {
[[ -r "$repo_root/scripts/standalone-app-payloads.txt" ]] || {
error 'standalone application payload manifest is missing.'
return 1
}
while IFS= read -r required_payload || [[ -n "$required_payload" ]]; do
[[ -n "$required_payload" && "$required_payload" != \#* ]] || continue
if [[ -z "${required_payloads[$required_payload]+set}" ]]; then
required_payloads["$required_payload"]=1
payload_files+=("$required_payload")
fi
done < "$repo_root/scripts/standalone-app-payloads.txt"
[[ -d "$application" && ! -L "$application" && -f "$application/bin/app" ]] || {
error "application is not a generated project: $application"
return 1
}
application_root="$(cd -- "$application" && pwd -P)" || return 1
standalone_destination_is_external "$application_root" "$destination" || return 2

for relative in "${payload_files[@]}"; do
if [[ -z "${payload_seen[$relative]+set}" ]]; then
validate_payload_file "$application_root" "$relative" || return 2
if [[ -z "${required_payloads[$relative]+set}" && "$relative" != assets/* && "$relative" != config/* ]]; then
error "optional application payload must be explicitly selected under assets/ or config/: $relative"
return 2
fi
payload_seen["$relative"]=1
selected_paths+=("$relative")
fi
done

[[ ! -e "$destination" && ! -L "$destination" ]] || {
error "refusing to overwrite standalone destination '$destination'"
return 2
Expand All @@ -208,7 +328,8 @@ standalone_bundle() {
error "unable to create a private staging directory beside '$destination'"
return 1
}
if ! copy_tree "$application" "$temporary"; then
application_payload="$temporary/.application-payload"
if ! copy_payload_files "$application_root" "$application_payload" "${selected_paths[@]}"; then
rm -rf -- "$temporary"
return 1
fi
Expand All @@ -220,13 +341,25 @@ standalone_bundle() {
! copy_verified_bundle "$framework_bundle" "$temporary" ||
! copy_verified_bundle "$framework_bundle" "$temporary/vendor/base-bash-libs" ||
! write_lock "$temporary/vendor/base-bash-libs" "$framework_bundle" standalone ||
! cp -- "$framework_bundle/bin/base-bash" "$temporary/bin/base-bash" ||
! cp -- "$application/VERSION" "$temporary/VERSION" ||
! cp -- "$application/README.md" "$temporary/README.md" ||
! chmod +x "$temporary/bin/base-bash" "$temporary/bin/app"; then
! chmod +x "$temporary/bin/base-bash"; then
rm -rf -- "$temporary"
return 1
fi
while IFS= read -r relative; do
[[ -n "$relative" ]] || continue
mkdir -p "$temporary/$(dirname -- "$relative")" || {
rm -rf -- "$temporary"
return 1
}
mv -- "$application_payload/$relative" "$temporary/$relative" || {
rm -rf -- "$temporary"
return 1
}
done < <(printf '%s\n' "${selected_paths[@]}" | LC_ALL=C sort -u)
rm -rf -- "$application_payload" || {
rm -rf -- "$temporary"
return 1
}
printf 'standalone_format=1\nframework_lock=%s\nprovenance=verified-offline-bundle\n' \
"$(hash_file "$framework_bundle/MANIFEST.sha256")" > "$temporary/BASE_BASH_STANDALONE.release" || {
rm -rf -- "$temporary"
Expand Down Expand Up @@ -270,11 +403,11 @@ main() {
verify_destination "$2"
;;
standalone)
(($# == 4)) || {
(($# >= 4)) || {
usage
return 2
}
standalone_bundle "$2" "$3" "$4"
standalone_bundle "$2" "$3" "$4" "${@:5}"
;;
*)
usage
Expand Down
3 changes: 2 additions & 1 deletion tests/launcher.bats
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ launcher_file_mode() {
copy_launcher_package() {
local package_root="$1"

mkdir -p "$package_root/bin" "$package_root/lib/bash"
mkdir -p "$package_root/bin" "$package_root/lib/bash" "$package_root/scripts"
cp "$BASE_REPO_ROOT/bin/base-bash" "$package_root/bin/base-bash"
cp "$BASE_REPO_ROOT/VERSION" "$package_root/VERSION"
cp "$BASE_REPO_ROOT/lib/bash/base-bash-libs.release" "$package_root/lib/bash/base-bash-libs.release"
cp "$BASE_REPO_ROOT/scripts/standalone-app-payloads.txt" "$package_root/scripts/standalone-app-payloads.txt"
chmod +x "$package_root/bin/base-bash"
}

Expand Down
Loading
Loading