From 2e36dcf909627c8f0e5662fe6b4825b1c1c24420 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 23 Oct 2026 11:16:54 -0400 Subject: [PATCH] packaging: harden shell scripts (SonarCloud S7688/S7679/S7682/S131) (#1640) - Use bash's [[ ]] instead of [ ] for conditional tests (S7688). - Copy positional parameters into named locals in render(), has() and sha256() (S7679). - End the env.sh helper functions with an explicit return, which keeps the last command's status (S7682). - Give the appimagetool checksum case a default branch for architectures without a pinned download (S131). No behaviour change; shellcheck -x is clean. --- packaging/env.sh | 19 ++++++++++++------- packaging/freebsd/package.sh | 14 +++++++------- packaging/icons.sh | 5 ++++- packaging/linux/flatpak-bundle.sh | 8 ++++---- packaging/linux/package.sh | 23 ++++++++++++++--------- packaging/macos/import-cert.sh | 6 +++--- packaging/macos/package.sh | 24 ++++++++++++------------ packaging/web/package.sh | 4 ++-- 8 files changed, 58 insertions(+), 45 deletions(-) diff --git a/packaging/env.sh b/packaging/env.sh index 0eb58029..55ff3943 100644 --- a/packaging/env.sh +++ b/packaging/env.sh @@ -19,10 +19,11 @@ workspace_version() { /^\[/ { in_pkg = ($0 == "[workspace.package]") ; next } in_pkg && $1 == "version" { gsub(/[" ]/, "", $3); print $3; exit } ' "$ROOT/Cargo.toml" + return } VERSION="${DESIGNCRAFT_VERSION:-$(workspace_version)}" -if [ -z "$VERSION" ]; then +if [[ -z "$VERSION" ]]; then echo "error: could not read [workspace.package] version from $ROOT/Cargo.toml" >&2 exit 1 fi @@ -32,7 +33,7 @@ DIST="${DIST:-$ROOT/dist/release}" mkdir -p "$DIST" export DIST -if [ -z "${DESIGNCRAFT_BUILD_SHA:-}" ]; then +if [[ -z "${DESIGNCRAFT_BUILD_SHA:-}" ]]; then DESIGNCRAFT_BUILD_SHA="$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || true)" fi export DESIGNCRAFT_BUILD_SHA @@ -41,29 +42,33 @@ export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}" # Emit a GitHub Actions warning (plain stderr outside Actions). warn() { - if [ -n "${GITHUB_ACTIONS:-}" ]; then echo "::warning::$*"; else echo "warning: $*" >&2; fi + if [[ -n "${GITHUB_ACTIONS:-}" ]]; then echo "::warning::$*"; else echo "warning: $*" >&2; fi + return } # Copy licence and readme files that exist into a package directory. copy_docs() { local dest="$1" f for f in README.md LICENSE LICENSE-MIT LICENSE-APACHE COPYRIGHT; do - if [ -f "$ROOT/$f" ]; then cp "$ROOT/$f" "$dest/"; fi + if [[ -f "$ROOT/$f" ]]; then cp "$ROOT/$f" "$dest/"; fi done copy_font_licences "$dest" + return } # Builds made with craft-fonts (CRAFT_FONTS_DIR, set by the release workflow) embed its fonts, so # the package carries their licences as OFL-.txt. Nothing without CRAFT_FONTS_DIR. copy_font_licences() { local dest="$1" f - [ -n "${CRAFT_FONTS_DIR:-}" ] || return 0 + [[ -n "${CRAFT_FONTS_DIR:-}" ]] || return 0 for f in "$CRAFT_FONTS_DIR"/fonts/*/OFL.txt; do - if [ -f "$f" ]; then cp "$f" "$dest/OFL-$(basename "$(dirname "$f")").txt"; fi + if [[ -f "$f" ]]; then cp "$f" "$dest/OFL-$(basename "$(dirname "$f")").txt"; fi done } # Portable SHA-256 of a file (prints just the hash). sha256() { - if command -v sha256sum >/dev/null; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi + local file="$1" + if command -v sha256sum >/dev/null; then sha256sum "$file" | cut -d' ' -f1; else shasum -a 256 "$file" | cut -d' ' -f1; fi + return } diff --git a/packaging/freebsd/package.sh b/packaging/freebsd/package.sh index 1854c284..d11434be 100755 --- a/packaging/freebsd/package.sh +++ b/packaging/freebsd/package.sh @@ -20,7 +20,7 @@ APP_ID=ai.storyteller.designcraft SKIP_BUILD=0 DRY_RUN=0 -while [ $# -gt 0 ]; do +while [[ $# -gt 0 ]]; do case "$1" in --skip-build) SKIP_BUILD=1; shift ;; --dry-run) DRY_RUN=1; SKIP_BUILD=1; shift ;; @@ -36,7 +36,7 @@ case "$MACHINE" in arm64 | aarch64) ARCH=aarch64 ;; *) echo "unsupported architecture $MACHINE" >&2; exit 2 ;; esac -if [ "$DRY_RUN" = 0 ] && [ "$(uname -s)" != FreeBSD ]; then +if [[ "$DRY_RUN" = 0 ]] && [[ "$(uname -s)" != FreeBSD ]]; then echo "error: build on FreeBSD (or pass --dry-run to check the tree layout)" >&2 exit 2 fi @@ -46,7 +46,7 @@ echo "==> DesignCraft $VERSION for FreeBSD $ARCH" # The release VM has 12 GB; full parallelism on the biggest crates runs it out of memory. export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-4}" -if [ "$SKIP_BUILD" = 0 ]; then +if [[ "$SKIP_BUILD" = 0 ]]; then (cd "$ROOT" && cargo build --release --locked -p designcraft -p designcraft-cli) fi WORK="$CARGO_TARGET_DIR/freebsd-package" @@ -56,7 +56,7 @@ mkdir -p "$WORK" BIN="$CARGO_TARGET_DIR/release" OUT_DIR="$DIST" -if [ "$DRY_RUN" = 1 ]; then +if [[ "$DRY_RUN" = 1 ]]; then # Stub binaries, and the tarball stays in the work dir so dist/ only ever holds real builds. BIN="$WORK/stub-bin" OUT_DIR="$WORK" @@ -81,13 +81,13 @@ sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$DESIGNCRAFT_BUILD_DATE/g" \ cp -R "$ROOT/assets/app-icon/hicolor" "$STAGE/share/icons/" copy_docs "$STAGE/share/doc/designcraft" for f in NOTICE ATTRIBUTION.md; do - if [ -f "$ROOT/$f" ]; then install -m 644 "$ROOT/$f" "$STAGE/share/doc/designcraft/"; fi + if [[ -f "$ROOT/$f" ]]; then install -m 644 "$ROOT/$f" "$STAGE/share/doc/designcraft/"; fi done for f in bin/designcraft bin/designcraft-cli "share/applications/$APP_ID.desktop" \ "share/icons/hicolor/256x256/apps/$APP_ID.png" "share/icons/hicolor/scalable/apps/$APP_ID.svg" \ share/doc/designcraft/LICENSE-MIT share/doc/designcraft/LICENSE-APACHE; do - if [ ! -e "$STAGE/$f" ]; then echo "error: $f is missing from the package" >&2; exit 1; fi + if [[ ! -e "$STAGE/$f" ]]; then echo "error: $f is missing from the package" >&2; exit 1; fi done # ---- .tar.gz ------------------------------------------------------------------------------------ @@ -96,7 +96,7 @@ tar -C "$WORK" -czf "$OUT_DIR/$BASENAME.tar.gz" "$BASENAME" echo "wrote $OUT_DIR/$BASENAME.tar.gz" "$STAGE/bin/designcraft-cli" --version -if [ "$DRY_RUN" = 1 ]; then +if [[ "$DRY_RUN" = 1 ]]; then echo "==> dry run: tarball contents" tar -tzvf "$OUT_DIR/$BASENAME.tar.gz" fi diff --git a/packaging/icons.sh b/packaging/icons.sh index 6e06fb9a..7ac9e295 100755 --- a/packaging/icons.sh +++ b/packaging/icons.sh @@ -21,7 +21,10 @@ MAC="$TMP/mac.svg" sed 's/viewBox="0 0 512 512"/viewBox="-62.136 -62.136 636.272 636.272"/' "$SVG" >"$MAC" grep -q 'viewBox="-62.136' "$MAC" || { echo "error: expected viewBox=\"0 0 512 512\" in $SVG" >&2; exit 1; } -render() { resvg -w "$2" -h "$2" "$1" "$3" &2; exit 1; } +[[ -f "$TARBALL" ]] || { echo "error: $TARBALL not found (run packaging/linux/package.sh --formats tar first)" >&2; exit 1; } for tool in flatpak flatpak-builder; do command -v "$tool" >/dev/null || { echo "error: $tool not found (apt install flatpak flatpak-builder librsvg2-common)" >&2; exit 1; } done @@ -49,7 +49,7 @@ WORK="$CARGO_TARGET_DIR/flatpak-bundle" rm -rf "$WORK" mkdir -p "$WORK/stage" tar -xzf "$TARBALL" -C "$WORK/stage" --strip-components=1 -[ -x "$WORK/stage/bin/designcraft" ] || { echo "error: $TARBALL has no bin/designcraft" >&2; exit 1; } +[[ -x "$WORK/stage/bin/designcraft" ]] || { echo "error: $TARBALL has no bin/designcraft" >&2; exit 1; } cp "$HERE/flatpak/$APP_ID.bundle.yml" "$WORK/$APP_ID.yml" flatpak remote-add --user --if-not-exists flathub "$FLATHUB" @@ -62,7 +62,7 @@ OUT="$DIST/$BASENAME.flatpak" flatpak build-bundle --runtime-repo="$FLATHUB" "$WORK/repo" "$OUT" "$APP_ID" stable echo "wrote $OUT" -if [ "$TEST" = 1 ]; then +if [[ "$TEST" = 1 ]]; then flatpak install --user -y --noninteractive --reinstall "$OUT" flatpak info --user "$APP_ID" flatpak run --command=designcraft-cli "$APP_ID" --version diff --git a/packaging/linux/package.sh b/packaging/linux/package.sh index 12ad8e0f..c4d843ec 100755 --- a/packaging/linux/package.sh +++ b/packaging/linux/package.sh @@ -22,7 +22,7 @@ APP_ID=ai.storyteller.designcraft SKIP_BUILD=0 FORMATS="appimage deb rpm arch tar" -while [ $# -gt 0 ]; do +while [[ $# -gt 0 ]]; do case "$1" in --skip-build) SKIP_BUILD=1; shift ;; --formats) FORMATS="$2"; shift 2 ;; @@ -43,8 +43,8 @@ BASENAME="designcraft-$VERSION-linux-$ARCH" echo "==> DesignCraft $VERSION for Linux $ARCH ($FORMATS)" -if [ "$SKIP_BUILD" = 0 ]; then - if [ -n "${CROSS_TARGET:-}" ]; then +if [[ "$SKIP_BUILD" = 0 ]]; then + if [[ -n "${CROSS_TARGET:-}" ]]; then (cd "$ROOT" && cargo build --release --locked -p designcraft -p designcraft-cli --target "$CROSS_TARGET") else (cd "$ROOT" && cargo build --release --locked -p designcraft -p designcraft-cli) @@ -76,7 +76,10 @@ if command -v appstreamcli >/dev/null; then appstreamcli validate --no-net --explain "$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml" fi -has() { case " $FORMATS " in *" $1 "*) return 0 ;; *) return 1 ;; esac; } +has() { + local fmt="$1" + case " $FORMATS " in *" $fmt "*) return 0 ;; *) return 1 ;; esac +} # ---- .tar.gz ------------------------------------------------------------------------------------ if has tar; then @@ -134,6 +137,8 @@ if has appimage; then case "$ARCH" in x86_64) APPIMAGETOOL_SHA256=ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 ;; aarch64) APPIMAGETOOL_SHA256=f0837e7448a0c1e4e650a93bb3e85802546e60654ef287576f46c71c126a9158 ;; + # No pinned download (e.g. riscv64): set APPIMAGETOOL or put appimagetool on PATH. + *) APPIMAGETOOL_SHA256="" ;; esac APPDIR="$WORK/DesignCraft.AppDir" cp -R "$STAGE" "$APPDIR" @@ -144,9 +149,9 @@ if has appimage; then ln -s "$APP_ID.png" "$APPDIR/.DirIcon" TOOL="${APPIMAGETOOL:-$(command -v appimagetool || true)}" - if [ -z "$TOOL" ]; then + if [[ -z "$TOOL" ]]; then TOOL="$CARGO_TARGET_DIR/appimagetool-$APPIMAGETOOL_VERSION-$ARCH.AppImage" - if [ ! -x "$TOOL" ]; then + if [[ ! -x "$TOOL" ]]; then download="$TOOL.download" curl --proto '=https' -fsSL -o "$download" \ "https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage" @@ -174,16 +179,16 @@ if has appimage; then # package) appimagetool also writes the .zsync, into its working directory, hence the cd. (cd "$DIST" && ARCH="$ARCH" APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" --no-appstream -u "$UPDATE_INFO" "$APPDIR" "$OUT") echo "wrote $OUT" - if [ -s "$OUT.zsync" ]; then + if [[ -s "$OUT.zsync" ]]; then echo "wrote $OUT.zsync" else warn "zsyncmake not found, so $OUT.zsync was not written; AppImage delta updates need it" fi fi -if [ -z "${CROSS_TARGET:-}" ]; then +if [[ -z "${CROSS_TARGET:-}" ]]; then "$STAGE/usr/bin/designcraft-cli" --version -elif [ -n "${EMULATOR:-}" ]; then +elif [[ -n "${EMULATOR:-}" ]]; then "$EMULATOR" "$STAGE/usr/bin/designcraft-cli" --version fi echo "==> done" diff --git a/packaging/macos/import-cert.sh b/packaging/macos/import-cert.sh index 9e549610..8669d867 100755 --- a/packaging/macos/import-cert.sh +++ b/packaging/macos/import-cert.sh @@ -12,7 +12,7 @@ umask 077 # shellcheck source=../env.sh . "$(dirname "${BASH_SOURCE[0]}")/../env.sh" -if [ -z "${APPLE_CERTIFICATE:-}" ]; then +if [[ -z "${APPLE_CERTIFICATE:-}" ]]; then warn "APPLE_CERTIFICATE is not set: macOS builds will be ad-hoc signed and not notarized" exit 0 fi @@ -36,13 +36,13 @@ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PASS" security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"') IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" | awk '/Developer ID Application/ { print $2; exit }')" -if [ -z "$IDENTITY" ]; then +if [[ -z "$IDENTITY" ]]; then security find-identity -v -p codesigning "$KEYCHAIN" >&2 echo "error: no 'Developer ID Application' identity in APPLE_CERTIFICATE" >&2 exit 1 fi echo "Imported signing identity $IDENTITY" -if [ -n "${GITHUB_ENV:-}" ]; then +if [[ -n "${GITHUB_ENV:-}" ]]; then { echo "MACOS_SIGN_IDENTITY=$IDENTITY"; echo "MACOS_KEYCHAIN=$KEYCHAIN"; } >>"$GITHUB_ENV" else echo "MACOS_SIGN_IDENTITY=$IDENTITY" diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh index c818ae6d..21edfa57 100755 --- a/packaging/macos/package.sh +++ b/packaging/macos/package.sh @@ -19,7 +19,7 @@ HERE="$ROOT/packaging/macos" ARCH=universal SKIP_BUILD=0 -while [ $# -gt 0 ]; do +while [[ $# -gt 0 ]]; do case "$1" in --arch) ARCH="$2"; shift 2 ;; --skip-build) SKIP_BUILD=1; shift ;; @@ -44,9 +44,9 @@ DMG="$DIST/designcraft-$VERSION-macos-$ARCH.dmg" CLI_ZIP="$DIST/designcraft-cli-$VERSION-macos-$ARCH.zip" NOTARIZE=0 -if [ "$IDENTITY" = "-" ]; then +if [[ "$IDENTITY" = "-" ]]; then warn "macOS: ad-hoc signing (no MACOS_SIGN_IDENTITY); artifacts are not notarized" -elif [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then +elif [[ -n "${APPLE_ID:-}" ]] && [[ -n "${APPLE_PASSWORD:-}" ]] && [[ -n "${APPLE_TEAM_ID:-}" ]]; then NOTARIZE=1 else warn "macOS: APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID incomplete; signed but not notarized" @@ -55,7 +55,7 @@ fi echo "==> DesignCraft $VERSION for macOS ($ARCH), identity: $IDENTITY, notarize: $NOTARIZE" # ---- build ------------------------------------------------------------------------------------- -if [ "$SKIP_BUILD" = 0 ]; then +if [[ "$SKIP_BUILD" = 0 ]]; then args=() for t in "${TARGETS[@]}"; do args+=(--target "$t"); done (cd "$ROOT" && cargo build --release --locked -p designcraft -p designcraft-cli "${args[@]}") @@ -74,9 +74,9 @@ done sign() { # Hardened runtime + secure timestamp for a real identity; ad-hoc can't be timestamped. local ts=(--timestamp) - [ "$IDENTITY" = "-" ] && ts=(--timestamp=none) + [[ "$IDENTITY" = "-" ]] && ts=(--timestamp=none) local kc=() - [ -n "${MACOS_KEYCHAIN:-}" ] && kc=(--keychain "$MACOS_KEYCHAIN") + [[ -n "${MACOS_KEYCHAIN:-}" ]] && kc=(--keychain "$MACOS_KEYCHAIN") codesign --force --sign "$IDENTITY" ${kc[@]+"${kc[@]}"} "${ts[@]}" "$@" } @@ -88,8 +88,8 @@ notarize() { echo "$out" id="$(printf '%s' "$out" | plutil -extract id raw -o - - 2>/dev/null || true)" status="$(printf '%s' "$out" | plutil -extract status raw -o - - 2>/dev/null || true)" - if [ "$status" != "Accepted" ]; then - if [ -n "$id" ]; then + if [[ "$status" != "Accepted" ]]; then + if [[ -n "$id" ]]; then xcrun notarytool log "$id" --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" --team-id "$APPLE_TEAM_ID" || true fi echo "error: notarization of $(basename "$file") failed (status: ${status:-unknown})" >&2 @@ -117,7 +117,7 @@ sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP/Contents/ sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP" codesign --verify --strict --deep --verbose=2 "$APP" -if [ "$NOTARIZE" = 1 ]; then +if [[ "$NOTARIZE" = 1 ]]; then ditto -c -k --keepParent "$APP" "$WORK/DesignCraft-notarize.zip" notarize "$WORK/DesignCraft-notarize.zip" xcrun stapler staple "$APP" @@ -151,7 +151,7 @@ hdiutil_retry() { if hdiutil "$@"; then return 0 fi - [ "$attempt" = 5 ] && { echo "hdiutil $what failed 5 times" >&2; return 1; } + [[ "$attempt" = 5 ]] && { echo "hdiutil $what failed 5 times" >&2; return 1; } echo "hdiutil $what failed (attempt $attempt of 5); retrying in $((attempt * 10))s" >&2 sleep $((attempt * 10)) done @@ -169,7 +169,7 @@ if ! codesign --verify --strict --deep --verbose=2 "$MOUNT/DesignCraft.app"; the exit 1 fi hdiutil_retry detach detach "$MOUNT" >/dev/null -if [ "$NOTARIZE" = 1 ]; then +if [[ "$NOTARIZE" = 1 ]]; then notarize "$DMG" xcrun stapler staple "$DMG" xcrun stapler validate "$DMG" @@ -187,7 +187,7 @@ codesign --verify --strict --verbose=2 "$CLI_DIR/designcraft-cli" rm -f "$CLI_ZIP" ditto -c -k --keepParent "$CLI_DIR" "$CLI_ZIP" # A bare Mach-O can't carry a stapled ticket; Gatekeeper looks the notarization up online. -if [ "$NOTARIZE" = 1 ]; then notarize "$CLI_ZIP"; fi +if [[ "$NOTARIZE" = 1 ]]; then notarize "$CLI_ZIP"; fi "$WORK/bin/designcraft-cli" --version echo "==> done" diff --git a/packaging/web/package.sh b/packaging/web/package.sh index 4aafc3e8..62c220f0 100755 --- a/packaging/web/package.sh +++ b/packaging/web/package.sh @@ -11,13 +11,13 @@ set -euo pipefail . "$(dirname "${BASH_SOURCE[0]}")/../env.sh" HERE="$ROOT/packaging/web" -if [ "${1:-}" != "--skip-build" ]; then +if [[ "${1:-}" != "--skip-build" ]]; then command -v trunk >/dev/null || { echo "error: trunk not found (cargo install trunk --locked)" >&2; exit 1; } (cd "$ROOT/apps/designcraft-web" && trunk build --release) fi SITE="$ROOT/dist/web" -[ -f "$SITE/index.html" ] || { echo "error: $SITE/index.html missing; run without --skip-build" >&2; exit 1; } +[[ -f "$SITE/index.html" ]] || { echo "error: $SITE/index.html missing; run without --skip-build" >&2; exit 1; } # Paths must be relative so the site works under any prefix (public_url = "./" in Trunk.toml). if grep -Eq '(src|href)="/[^/]' "$SITE/index.html"; then echo "error: $SITE/index.html has root-absolute URLs; it would break when served from a sub-path" >&2