From 011a3f9d9f4699d472104e1cd6b567328735ef0f Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 2 Mar 2026 16:04:53 -0700 Subject: [PATCH] chore: split out nvidia variant --- .github/workflows/startos-iso.yaml | 17 ++++++- Makefile | 2 +- build/dpkg-deps/generate.sh | 4 ++ build/dpkg-deps/nonfree.depends | 5 +- build/dpkg-deps/nvidia.depends | 1 + build/image-recipe/build.sh | 23 +++++---- build/upload-ota.sh | 26 ++++++++--- container-runtime/__mocks__/mime.js | 30 ++++++++++++ container-runtime/jest.config.js | 3 ++ core/src/lib.rs | 3 ++ debian/dpkg-build.sh | 4 +- sdk/base/lib/index.ts | 72 +++++++++++++++++++++++++++-- 12 files changed, 164 insertions(+), 26 deletions(-) create mode 100644 build/dpkg-deps/nvidia.depends create mode 100644 container-runtime/__mocks__/mime.js diff --git a/.github/workflows/startos-iso.yaml b/.github/workflows/startos-iso.yaml index be64862ce..40dec852b 100644 --- a/.github/workflows/startos-iso.yaml +++ b/.github/workflows/startos-iso.yaml @@ -25,10 +25,13 @@ on: - ALL - x86_64 - x86_64-nonfree + - x86_64-nvidia - aarch64 - aarch64-nonfree + - aarch64-nvidia # - raspberrypi - riscv64 + - riscv64-nonfree deploy: type: choice description: Deploy @@ -65,10 +68,13 @@ jobs: fromJson('{ "x86_64": ["x86_64"], "x86_64-nonfree": ["x86_64"], + "x86_64-nvidia": ["x86_64"], "aarch64": ["aarch64"], "aarch64-nonfree": ["aarch64"], + "aarch64-nvidia": ["aarch64"], "raspberrypi": ["aarch64"], "riscv64": ["riscv64"], + "riscv64-nonfree": ["riscv64"], "ALL": ["x86_64", "aarch64", "riscv64"] }')[github.event.inputs.platform || 'ALL'] }} @@ -125,7 +131,7 @@ jobs: format( '[ ["{0}"], - ["x86_64", "x86_64-nonfree", "aarch64", "aarch64-nonfree", "riscv64"] + ["x86_64", "x86_64-nonfree", "x86_64-nvidia", "aarch64", "aarch64-nonfree", "aarch64-nvidia", "riscv64", "riscv64-nonfree"] ]', github.event.inputs.platform || 'ALL' ) @@ -139,18 +145,24 @@ jobs: fromJson('{ "x86_64": "ubuntu-latest", "x86_64-nonfree": "ubuntu-latest", + "x86_64-nvidia": "ubuntu-latest", "aarch64": "ubuntu-24.04-arm", "aarch64-nonfree": "ubuntu-24.04-arm", + "aarch64-nvidia": "ubuntu-24.04-arm", "raspberrypi": "ubuntu-24.04-arm", "riscv64": "ubuntu-24.04-arm", + "riscv64-nonfree": "ubuntu-24.04-arm", }')[matrix.platform], fromJson('{ "x86_64": "buildjet-8vcpu-ubuntu-2204", "x86_64-nonfree": "buildjet-8vcpu-ubuntu-2204", + "x86_64-nvidia": "buildjet-8vcpu-ubuntu-2204", "aarch64": "buildjet-8vcpu-ubuntu-2204-arm", "aarch64-nonfree": "buildjet-8vcpu-ubuntu-2204-arm", + "aarch64-nvidia": "buildjet-8vcpu-ubuntu-2204-arm", "raspberrypi": "buildjet-8vcpu-ubuntu-2204-arm", "riscv64": "buildjet-8vcpu-ubuntu-2204", + "riscv64-nonfree": "buildjet-8vcpu-ubuntu-2204", }')[matrix.platform] ) )[github.event.inputs.runner == 'fast'] @@ -161,10 +173,13 @@ jobs: fromJson('{ "x86_64": "x86_64", "x86_64-nonfree": "x86_64", + "x86_64-nvidia": "x86_64", "aarch64": "aarch64", "aarch64-nonfree": "aarch64", + "aarch64-nvidia": "aarch64", "raspberrypi": "aarch64", "riscv64": "riscv64", + "riscv64-nonfree": "riscv64", }')[matrix.platform] }} steps: diff --git a/Makefile b/Makefile index b3c56edd0..7ab474909 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ GIT_HASH_FILE := $(shell ./build/env/check-git-hash.sh) VERSION_FILE := $(shell ./build/env/check-version.sh) BASENAME := $(shell PROJECT=startos ./build/env/basename.sh) PLATFORM := $(shell if [ -f $(PLATFORM_FILE) ]; then cat $(PLATFORM_FILE); else echo unknown; fi) -ARCH := $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo aarch64; else echo $(PLATFORM) | sed 's/-nonfree$$//g'; fi) +ARCH := $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo aarch64; elif [ "$(PLATFORM)" = "rockchip64" ]; then echo aarch64; else echo $(PLATFORM) | sed 's/-nonfree$$//g; s/-nvidia$$//g'; fi) RUST_ARCH := $(shell if [ "$(ARCH)" = "riscv64" ]; then echo riscv64gc; else echo $(ARCH); fi) REGISTRY_BASENAME := $(shell PROJECT=start-registry PLATFORM=$(ARCH) ./build/env/basename.sh) TUNNEL_BASENAME := $(shell PROJECT=start-tunnel PLATFORM=$(ARCH) ./build/env/basename.sh) diff --git a/build/dpkg-deps/generate.sh b/build/dpkg-deps/generate.sh index ffb80dce3..b7a4925b2 100755 --- a/build/dpkg-deps/generate.sh +++ b/build/dpkg-deps/generate.sh @@ -12,6 +12,10 @@ fi if [[ "$PLATFORM" =~ -nonfree$ ]]; then FEATURES+=("nonfree") fi +if [[ "$PLATFORM" =~ -nvidia$ ]]; then + FEATURES+=("nonfree") + FEATURES+=("nvidia") +fi feature_file_checker=' /^#/ { next } diff --git a/build/dpkg-deps/nonfree.depends b/build/dpkg-deps/nonfree.depends index 73d021d02..484c14249 100644 --- a/build/dpkg-deps/nonfree.depends +++ b/build/dpkg-deps/nonfree.depends @@ -4,7 +4,4 @@ + firmware-iwlwifi + firmware-libertas + firmware-misc-nonfree -+ firmware-realtek -+ nvidia-container-toolkit -# + nvidia-driver -# + nvidia-kernel-dkms \ No newline at end of file ++ firmware-realtek \ No newline at end of file diff --git a/build/dpkg-deps/nvidia.depends b/build/dpkg-deps/nvidia.depends new file mode 100644 index 000000000..ad0324664 --- /dev/null +++ b/build/dpkg-deps/nvidia.depends @@ -0,0 +1 @@ ++ nvidia-container-toolkit diff --git a/build/image-recipe/build.sh b/build/image-recipe/build.sh index 7a81e50dc..eb5b7fff2 100755 --- a/build/image-recipe/build.sh +++ b/build/image-recipe/build.sh @@ -34,11 +34,11 @@ fi IMAGE_BASENAME=startos-${VERSION_FULL}_${IB_TARGET_PLATFORM} BOOTLOADERS=grub-efi -if [ "$IB_TARGET_PLATFORM" = "x86_64" ] || [ "$IB_TARGET_PLATFORM" = "x86_64-nonfree" ]; then +if [ "$IB_TARGET_PLATFORM" = "x86_64" ] || [ "$IB_TARGET_PLATFORM" = "x86_64-nonfree" ] || [ "$IB_TARGET_PLATFORM" = "x86_64-nvidia" ]; then IB_TARGET_ARCH=amd64 QEMU_ARCH=x86_64 BOOTLOADERS=grub-efi,syslinux -elif [ "$IB_TARGET_PLATFORM" = "aarch64" ] || [ "$IB_TARGET_PLATFORM" = "aarch64-nonfree" ] || [ "$IB_TARGET_PLATFORM" = "raspberrypi" ] || [ "$IB_TARGET_PLATFORM" = "rockchip64" ]; then +elif [ "$IB_TARGET_PLATFORM" = "aarch64" ] || [ "$IB_TARGET_PLATFORM" = "aarch64-nonfree" ] || [ "$IB_TARGET_PLATFORM" = "aarch64-nvidia" ] || [ "$IB_TARGET_PLATFORM" = "raspberrypi" ] || [ "$IB_TARGET_PLATFORM" = "rockchip64" ]; then IB_TARGET_ARCH=arm64 QEMU_ARCH=aarch64 elif [ "$IB_TARGET_PLATFORM" = "riscv64" ] || [ "$IB_TARGET_PLATFORM" = "riscv64-nonfree" ]; then @@ -60,9 +60,13 @@ mkdir -p $prep_results_dir cd $prep_results_dir NON_FREE= -if [[ "${IB_TARGET_PLATFORM}" =~ -nonfree$ ]] || [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then +if [[ "${IB_TARGET_PLATFORM}" =~ -nonfree$ ]] || [[ "${IB_TARGET_PLATFORM}" =~ -nvidia$ ]] || [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then NON_FREE=1 fi +NVIDIA= +if [[ "${IB_TARGET_PLATFORM}" =~ -nvidia$ ]]; then + NVIDIA=1 +fi IMAGE_TYPE=iso if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ] || [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then IMAGE_TYPE=img @@ -177,7 +181,7 @@ if [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then echo "deb https://apt.armbian.com/ ${IB_SUITE} main" > config/archives/armbian.list fi -if [ "$NON_FREE" = 1 ]; then +if [ "$NVIDIA" = 1 ]; then curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --dearmor -o config/archives/nvidia-container-toolkit.key curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \ | sed 's#deb https://#deb [signed-by=/etc/apt/trusted.gpg.d/nvidia-container-toolkit.key.gpg] https://#g' \ @@ -205,11 +209,11 @@ cat > config/hooks/normal/9000-install-startos.hook.chroot << EOF set -e -if [ "${NON_FREE}" = "1" ] && [ "${IB_TARGET_PLATFORM}" != "raspberrypi" ] && [ "${IB_TARGET_PLATFORM}" != "riscv64-nonfree" ]; then +if [ "${NVIDIA}" = "1" ]; then # install a specific NVIDIA driver version # ---------------- configuration ---------------- - NVIDIA_DRIVER_VERSION="\${NVIDIA_DRIVER_VERSION:-580.119.02}" + NVIDIA_DRIVER_VERSION="\${NVIDIA_DRIVER_VERSION:-580.126.09}" BASE_URL="https://download.nvidia.com/XFree86/Linux-${QEMU_ARCH}" @@ -259,12 +263,15 @@ if [ "${NON_FREE}" = "1" ] && [ "${IB_TARGET_PLATFORM}" != "raspberrypi" ] && [ echo "[nvidia-hook] Running NVIDIA installer for kernel \${KVER}" >&2 - sh "\${RUN_PATH}" \ + if ! sh "\${RUN_PATH}" \ --silent \ --kernel-name="\${KVER}" \ --no-x-check \ --no-nouveau-check \ - --no-runlevel-check + --no-runlevel-check; then + cat /var/log/nvidia-installer.log + exit 1 + fi # Rebuild module metadata echo "[nvidia-hook] Running depmod for \${KVER}" >&2 diff --git a/build/upload-ota.sh b/build/upload-ota.sh index 74e1b42aa..f1ee185dc 100755 --- a/build/upload-ota.sh +++ b/build/upload-ota.sh @@ -15,10 +15,10 @@ if [ "$SKIP_DL" != "1" ]; then fi if [ -n "$RUN_ID" ]; then - for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do + for arch in aarch64 aarch64-nonfree aarch64-nvidia riscv64 riscv64-nonfree x86_64 x86_64-nonfree x86_64-nvidia; do while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.squashfs -D $(pwd); do sleep 1; done done - for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do + for arch in aarch64 aarch64-nonfree aarch64-nvidia riscv64 riscv64-nonfree x86_64 x86_64-nonfree x86_64-nvidia; do while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.iso -D $(pwd); do sleep 1; done done fi @@ -65,18 +65,29 @@ elif [ "$SKIP_UL" != "1" ]; then fi if [ "$SKIP_INDEX" != "1" ]; then - for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do + for arch in aarch64 aarch64-nonfree aarch64-nvidia riscv64 riscv64-nonfree x86_64 x86_64-nonfree x86_64-nvidia; do for file in *_$arch.squashfs *_$arch.iso; do start-cli --registry=https://alpha-registry-x.start9.com registry os asset add --platform=$arch --version=$VERSION $file https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$file done done fi +GH_USER=${GH_USER:-$(gh api user -q .login 2>/dev/null || true)} +GH_GPG_KEY=$(git config user.signingkey 2>/dev/null || true) + for file in *.iso *.squashfs *.deb start-cli_*; do - gpg -u 7CFFDA41CA66056A --detach-sign --armor -o "${file}.asc" "$file" + gpg -u 2D63C217 --detach-sign --armor -o "${file}.start9.asc" "$file" + if [ -n "$GH_USER" ] && [ -n "$GH_GPG_KEY" ]; then + gpg -u "$GH_GPG_KEY" --detach-sign --armor -o "${file}.${GH_USER}.asc" "$file" + fi done -gpg --export -a 7CFFDA41CA66056A > dr-bonez.key.asc +gpg --export -a 2D63C217 > start9.key.asc +if [ -n "$GH_USER" ] && [ -n "$GH_GPG_KEY" ]; then + gpg --export -a "$GH_GPG_KEY" > "${GH_USER}.key.asc" +else + >&2 echo 'Warning: could not determine GitHub user or GPG signing key, skipping personal signature' +fi tar -czvf signatures.tar.gz *.asc gh release upload -R Start9Labs/start-os v$VERSION signatures.tar.gz @@ -85,10 +96,13 @@ cat << EOF # ISO Downloads - [x86_64/AMD64](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_x86_64-nonfree.iso)) +- [x86_64/AMD64 + NVIDIA](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_x86_64-nvidia.iso)) - [x86_64/AMD64-slim (FOSS-only)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_x86_64.iso) "Without proprietary software or drivers") - [aarch64/ARM64](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_aarch64-nonfree.iso)) +- [aarch64/ARM64 + NVIDIA](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_aarch64-nvidia.iso)) - [aarch64/ARM64-slim (FOSS-Only)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_aarch64.iso) "Without proprietary software or drivers") -- [RISCV64 (RVA23)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_riscv64.iso)) +- [RISCV64 (RVA23)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_riscv64-nonfree.iso)) +- [RISCV64 (RVA23)-slim (FOSS-only)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_riscv64.iso) "Without proprietary software or drivers") EOF cat << 'EOF' diff --git a/container-runtime/__mocks__/mime.js b/container-runtime/__mocks__/mime.js new file mode 100644 index 000000000..d2f6ff46b --- /dev/null +++ b/container-runtime/__mocks__/mime.js @@ -0,0 +1,30 @@ +// Mock for ESM-only mime package — Jest's module loader doesn't support require(esm) +const types = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".webp": "image/webp", + ".ico": "image/x-icon", + ".json": "application/json", + ".js": "application/javascript", + ".html": "text/html", + ".css": "text/css", + ".txt": "text/plain", + ".md": "text/markdown", +} + +module.exports = { + default: { + getType(path) { + const ext = "." + path.split(".").pop() + return types[ext] || null + }, + getExtension(type) { + const entry = Object.entries(types).find(([, v]) => v === type) + return entry ? entry[0].slice(1) : null + }, + }, + __esModule: true, +} diff --git a/container-runtime/jest.config.js b/container-runtime/jest.config.js index f499f03f9..1e9bb209a 100644 --- a/container-runtime/jest.config.js +++ b/container-runtime/jest.config.js @@ -5,4 +5,7 @@ module.exports = { testEnvironment: "node", rootDir: "./src/", modulePathIgnorePatterns: ["./dist/"], + moduleNameMapper: { + "^mime$": "/../__mocks__/mime.js", + }, } diff --git a/core/src/lib.rs b/core/src/lib.rs index fa31d41c3..10913503d 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -25,6 +25,9 @@ pub fn platform_to_arch(platform: &str) -> &str { if let Some(arch) = platform.strip_suffix("-nonfree") { return arch; } + if let Some(arch) = platform.strip_suffix("-nvidia") { + return arch; + } match platform { "raspberrypi" | "rockchip64" => "aarch64", _ => platform, diff --git a/debian/dpkg-build.sh b/debian/dpkg-build.sh index 35ecef807..0e68ea173 100755 --- a/debian/dpkg-build.sh +++ b/debian/dpkg-build.sh @@ -7,9 +7,9 @@ cd "$(dirname "${BASH_SOURCE[0]}")/.." PROJECT=${PROJECT:-"startos"} BASENAME=${BASENAME:-"$(./build/env/basename.sh)"} VERSION=${VERSION:-$(cat ./build/env/VERSION.txt)} -if [ "$PLATFORM" = "x86_64" ] || [ "$PLATFORM" = "x86_64-nonfree" ]; then +if [ "$PLATFORM" = "x86_64" ] || [ "$PLATFORM" = "x86_64-nonfree" ] || [ "$PLATFORM" = "x86_64-nvidia" ]; then DEB_ARCH=amd64 -elif [ "$PLATFORM" = "aarch64" ] || [ "$PLATFORM" = "aarch64-nonfree" ] || [ "$PLATFORM" = "raspberrypi" ]; then +elif [ "$PLATFORM" = "aarch64" ] || [ "$PLATFORM" = "aarch64-nonfree" ] || [ "$PLATFORM" = "aarch64-nvidia" ] || [ "$PLATFORM" = "raspberrypi" ]; then DEB_ARCH=arm64 else DEB_ARCH="$PLATFORM" diff --git a/sdk/base/lib/index.ts b/sdk/base/lib/index.ts index 3ec7b0dae..9437d3e43 100644 --- a/sdk/base/lib/index.ts +++ b/sdk/base/lib/index.ts @@ -14,14 +14,78 @@ import type { DeepPartial } from './types' type ZodDeepPartial = (a: _z.ZodType) => _z.ZodType> -// Add deepPartial to z at runtime, wrapping with .passthrough() to allow extra keys -;(_z as any).deepPartial = (a: _z.ZodType) => - (zodDeepPartial(a) as any).passthrough() +// Recursively make all ZodObjects in a schema loose (preserve extra keys at every nesting level). +// Uses _zod.def.type duck-typing instead of instanceof to avoid issues with mismatched zod versions. +function deepLoose(schema: S): S { + const def = (schema as any)._zod?.def + if (!def) return schema + let result: _z.ZodType + switch (def.type) { + case 'optional': + result = deepLoose(def.innerType).optional() + break + case 'nullable': + result = deepLoose(def.innerType).nullable() + break + case 'object': { + const newShape: Record = {} + for (const key in (schema as any).shape) { + newShape[key] = deepLoose((schema as any).shape[key]) + } + result = _z.looseObject(newShape) + break + } + case 'array': + result = _z.array(deepLoose(def.element)) + break + case 'union': + result = _z.union(def.options.map((o: _z.ZodType) => deepLoose(o))) + break + case 'intersection': + result = _z.intersection(deepLoose(def.left), deepLoose(def.right)) + break + case 'record': + result = _z.record(def.keyType, deepLoose(def.valueType)) + break + case 'tuple': + result = _z.tuple(def.items.map((i: _z.ZodType) => deepLoose(i))) + break + case 'lazy': + result = _z.lazy(() => deepLoose(def.getter())) + break + default: + return schema + } + return result as S +} -// Augment zod's z namespace so z.deepPartial is typed +type ZodDeepLoose = (a: _z.ZodType) => _z.ZodType + +// Add deepPartial and deepLoose to z at runtime +;(_z as any).deepPartial = (a: _z.ZodType) => deepLoose(zodDeepPartial(a)) +;(_z as any).deepLoose = deepLoose + +// Augment zod's z namespace so z.deepPartial and z.deepLoose are typed declare module 'zod' { namespace z { const deepPartial: ZodDeepPartial + const deepLoose: ZodDeepLoose + } +} + +// Override z.object to produce loose objects by default (extra keys are preserved, not stripped). +// Patches the source module in require.cache where 'object' is a writable property; +// the CJS getter chain (index → external → schemas) then relays the patched version. +// We walk only the zod entry module's dependency tree and match by identity (=== origObject). +const _origObject = _z.object +const _zodModule = require.cache[require.resolve('zod')] +for (const child of _zodModule?.children ?? []) { + for (const grandchild of child.children ?? []) { + const desc = Object.getOwnPropertyDescriptor(grandchild.exports, 'object') + if (desc?.value === _origObject && desc.writable) { + grandchild.exports.object = (...args: Parameters) => + _origObject(...args).loose() + } } }