mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 12:33:40 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
201965b809 | ||
|
|
2baf1a880b | ||
|
|
58e0b166cb | ||
|
|
2a678bb017 | ||
|
|
5664456b77 | ||
|
|
3685b7e57e | ||
|
|
989d5f73b1 |
81
.github/actions/setup-build/action.yml
vendored
Normal file
81
.github/actions/setup-build/action.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
name: Setup Build Environment
|
||||||
|
description: Common build environment setup steps
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
nodejs-version:
|
||||||
|
description: Node.js version
|
||||||
|
required: true
|
||||||
|
setup-python:
|
||||||
|
description: Set up Python
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
setup-docker:
|
||||||
|
description: Set up Docker QEMU and Buildx
|
||||||
|
required: false
|
||||||
|
default: "true"
|
||||||
|
setup-sccache:
|
||||||
|
description: Configure sccache for GitHub Actions
|
||||||
|
required: false
|
||||||
|
default: "true"
|
||||||
|
free-space:
|
||||||
|
description: Remove unnecessary packages to free disk space
|
||||||
|
required: false
|
||||||
|
default: "true"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Free disk space
|
||||||
|
if: inputs.free-space == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
sudo apt-get remove --purge -y azure-cli || true
|
||||||
|
sudo apt-get remove --purge -y firefox || true
|
||||||
|
sudo apt-get remove --purge -y ghc-* || true
|
||||||
|
sudo apt-get remove --purge -y google-cloud-sdk || true
|
||||||
|
sudo apt-get remove --purge -y google-chrome-stable || true
|
||||||
|
sudo apt-get remove --purge -y powershell || true
|
||||||
|
sudo apt-get remove --purge -y php* || true
|
||||||
|
sudo apt-get remove --purge -y ruby* || true
|
||||||
|
sudo apt-get remove --purge -y mono-* || true
|
||||||
|
sudo apt-get autoremove -y
|
||||||
|
sudo apt-get clean
|
||||||
|
sudo rm -rf /usr/lib/jvm
|
||||||
|
sudo rm -rf /usr/local/.ghcup
|
||||||
|
sudo rm -rf /usr/local/lib/android
|
||||||
|
sudo rm -rf /usr/share/dotnet
|
||||||
|
sudo rm -rf /usr/share/swift
|
||||||
|
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||||
|
|
||||||
|
# BuildJet runners lack /opt/hostedtoolcache, which setup-python and setup-qemu expect
|
||||||
|
- name: Ensure hostedtoolcache exists
|
||||||
|
shell: bash
|
||||||
|
run: sudo mkdir -p /opt/hostedtoolcache && sudo chown $USER:$USER /opt/hostedtoolcache
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
if: inputs.setup-python == 'true'
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ inputs.nodejs-version }}
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: "**/package-lock.json"
|
||||||
|
|
||||||
|
- name: Set up Docker QEMU
|
||||||
|
if: inputs.setup-docker == 'true'
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
if: inputs.setup-docker == 'true'
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Configure sccache
|
||||||
|
if: inputs.setup-sccache == 'true'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
|
||||||
|
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||||
46
.github/workflows/start-cli.yaml
vendored
46
.github/workflows/start-cli.yaml
vendored
@@ -38,7 +38,7 @@ on:
|
|||||||
- next/*
|
- next/*
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.sha }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -48,6 +48,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
compile:
|
compile:
|
||||||
name: Build Debian Package
|
name: Build Debian Package
|
||||||
|
if: github.event.pull_request.draft != true
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
@@ -64,50 +65,15 @@ jobs:
|
|||||||
}}
|
}}
|
||||||
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
||||||
steps:
|
steps:
|
||||||
- name: Cleaning up unnecessary files
|
- name: Mount tmpfs
|
||||||
run: |
|
|
||||||
sudo apt-get remove --purge -y mono-* \
|
|
||||||
ghc* cabal-install* \
|
|
||||||
dotnet* \
|
|
||||||
php* \
|
|
||||||
ruby* \
|
|
||||||
mysql-* \
|
|
||||||
postgresql-* \
|
|
||||||
azure-cli \
|
|
||||||
powershell \
|
|
||||||
google-cloud-sdk \
|
|
||||||
msodbcsql* mssql-tools* \
|
|
||||||
imagemagick* \
|
|
||||||
libgl1-mesa-dri \
|
|
||||||
google-chrome-stable \
|
|
||||||
firefox
|
|
||||||
sudo apt-get autoremove -y
|
|
||||||
sudo apt-get clean
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
sudo mount -t tmpfs tmpfs .
|
|
||||||
if: ${{ github.event.inputs.runner == 'fast' }}
|
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||||
|
run: sudo mount -t tmpfs tmpfs .
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
- uses: ./.github/actions/setup-build
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODEJS_VERSION }}
|
nodejs-version: ${{ env.NODEJS_VERSION }}
|
||||||
|
|
||||||
- name: Set up docker QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Configure sccache
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
|
|
||||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
|
||||||
|
|
||||||
- name: Make
|
- name: Make
|
||||||
run: TARGET=${{ matrix.triple }} make cli
|
run: TARGET=${{ matrix.triple }} make cli
|
||||||
|
|||||||
46
.github/workflows/start-registry.yaml
vendored
46
.github/workflows/start-registry.yaml
vendored
@@ -36,7 +36,7 @@ on:
|
|||||||
- next/*
|
- next/*
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.sha }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -46,6 +46,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
compile:
|
compile:
|
||||||
name: Build Debian Package
|
name: Build Debian Package
|
||||||
|
if: github.event.pull_request.draft != true
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
@@ -60,50 +61,15 @@ jobs:
|
|||||||
}}
|
}}
|
||||||
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
||||||
steps:
|
steps:
|
||||||
- name: Cleaning up unnecessary files
|
- name: Mount tmpfs
|
||||||
run: |
|
|
||||||
sudo apt-get remove --purge -y mono-* \
|
|
||||||
ghc* cabal-install* \
|
|
||||||
dotnet* \
|
|
||||||
php* \
|
|
||||||
ruby* \
|
|
||||||
mysql-* \
|
|
||||||
postgresql-* \
|
|
||||||
azure-cli \
|
|
||||||
powershell \
|
|
||||||
google-cloud-sdk \
|
|
||||||
msodbcsql* mssql-tools* \
|
|
||||||
imagemagick* \
|
|
||||||
libgl1-mesa-dri \
|
|
||||||
google-chrome-stable \
|
|
||||||
firefox
|
|
||||||
sudo apt-get autoremove -y
|
|
||||||
sudo apt-get clean
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
sudo mount -t tmpfs tmpfs .
|
|
||||||
if: ${{ github.event.inputs.runner == 'fast' }}
|
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||||
|
run: sudo mount -t tmpfs tmpfs .
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
- uses: ./.github/actions/setup-build
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODEJS_VERSION }}
|
nodejs-version: ${{ env.NODEJS_VERSION }}
|
||||||
|
|
||||||
- name: Set up docker QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Configure sccache
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
|
|
||||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
|
||||||
|
|
||||||
- name: Make
|
- name: Make
|
||||||
run: make registry-deb
|
run: make registry-deb
|
||||||
|
|||||||
46
.github/workflows/start-tunnel.yaml
vendored
46
.github/workflows/start-tunnel.yaml
vendored
@@ -36,7 +36,7 @@ on:
|
|||||||
- next/*
|
- next/*
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.sha }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -46,6 +46,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
compile:
|
compile:
|
||||||
name: Build Debian Package
|
name: Build Debian Package
|
||||||
|
if: github.event.pull_request.draft != true
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
@@ -60,50 +61,15 @@ jobs:
|
|||||||
}}
|
}}
|
||||||
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
||||||
steps:
|
steps:
|
||||||
- name: Cleaning up unnecessary files
|
- name: Mount tmpfs
|
||||||
run: |
|
|
||||||
sudo apt-get remove --purge -y mono-* \
|
|
||||||
ghc* cabal-install* \
|
|
||||||
dotnet* \
|
|
||||||
php* \
|
|
||||||
ruby* \
|
|
||||||
mysql-* \
|
|
||||||
postgresql-* \
|
|
||||||
azure-cli \
|
|
||||||
powershell \
|
|
||||||
google-cloud-sdk \
|
|
||||||
msodbcsql* mssql-tools* \
|
|
||||||
imagemagick* \
|
|
||||||
libgl1-mesa-dri \
|
|
||||||
google-chrome-stable \
|
|
||||||
firefox
|
|
||||||
sudo apt-get autoremove -y
|
|
||||||
sudo apt-get clean
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
sudo mount -t tmpfs tmpfs .
|
|
||||||
if: ${{ github.event.inputs.runner == 'fast' }}
|
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||||
|
run: sudo mount -t tmpfs tmpfs .
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
- uses: ./.github/actions/setup-build
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODEJS_VERSION }}
|
nodejs-version: ${{ env.NODEJS_VERSION }}
|
||||||
|
|
||||||
- name: Set up docker QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Configure sccache
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
|
|
||||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
|
||||||
|
|
||||||
- name: Make
|
- name: Make
|
||||||
run: make tunnel-deb
|
run: make tunnel-deb
|
||||||
|
|||||||
60
.github/workflows/startos-iso.yaml
vendored
60
.github/workflows/startos-iso.yaml
vendored
@@ -27,7 +27,7 @@ on:
|
|||||||
- x86_64-nonfree
|
- x86_64-nonfree
|
||||||
- aarch64
|
- aarch64
|
||||||
- aarch64-nonfree
|
- aarch64-nonfree
|
||||||
- raspberrypi
|
# - raspberrypi
|
||||||
- riscv64
|
- riscv64
|
||||||
deploy:
|
deploy:
|
||||||
type: choice
|
type: choice
|
||||||
@@ -46,7 +46,7 @@ on:
|
|||||||
- next/*
|
- next/*
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.sha }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -56,6 +56,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
compile:
|
compile:
|
||||||
name: Compile Base Binaries
|
name: Compile Base Binaries
|
||||||
|
if: github.event.pull_request.draft != true
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
@@ -90,54 +91,16 @@ jobs:
|
|||||||
)[github.event.inputs.runner == 'fast']
|
)[github.event.inputs.runner == 'fast']
|
||||||
}}
|
}}
|
||||||
steps:
|
steps:
|
||||||
- name: Cleaning up unnecessary files
|
- name: Mount tmpfs
|
||||||
run: |
|
|
||||||
sudo apt-get remove --purge -y azure-cli || true
|
|
||||||
sudo apt-get remove --purge -y firefox || true
|
|
||||||
sudo apt-get remove --purge -y ghc-* || true
|
|
||||||
sudo apt-get remove --purge -y google-cloud-sdk || true
|
|
||||||
sudo apt-get remove --purge -y google-chrome-stable || true
|
|
||||||
sudo apt-get remove --purge -y powershell || true
|
|
||||||
sudo apt-get remove --purge -y php* || true
|
|
||||||
sudo apt-get remove --purge -y ruby* || true
|
|
||||||
sudo apt-get remove --purge -y mono-* || true
|
|
||||||
sudo apt-get autoremove -y
|
|
||||||
sudo apt-get clean
|
|
||||||
sudo rm -rf /usr/lib/jvm # All JDKs
|
|
||||||
sudo rm -rf /usr/local/.ghcup # Haskell toolchain
|
|
||||||
sudo rm -rf /usr/local/lib/android # Android SDK/NDK, emulator
|
|
||||||
sudo rm -rf /usr/share/dotnet # .NET SDKs
|
|
||||||
sudo rm -rf /usr/share/swift # Swift toolchain (if present)
|
|
||||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY" # Pre-cached tool cache (Go, Node, etc.)
|
|
||||||
- run: |
|
|
||||||
sudo mount -t tmpfs tmpfs .
|
|
||||||
if: ${{ github.event.inputs.runner == 'fast' }}
|
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||||
|
run: sudo mount -t tmpfs tmpfs .
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
- uses: ./.github/actions/setup-build
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
nodejs-version: ${{ env.NODEJS_VERSION }}
|
||||||
|
setup-python: "true"
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.NODEJS_VERSION }}
|
|
||||||
|
|
||||||
- name: Set up docker QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Configure sccache
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
|
|
||||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
|
||||||
|
|
||||||
- name: Make
|
- name: Make
|
||||||
run: make ARCH=${{ matrix.arch }} compiled-${{ matrix.arch }}.tar
|
run: make ARCH=${{ matrix.arch }} compiled-${{ matrix.arch }}.tar
|
||||||
@@ -155,13 +118,14 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
# TODO: re-add "raspberrypi" to the platform list below
|
||||||
platform: >-
|
platform: >-
|
||||||
${{
|
${{
|
||||||
fromJson(
|
fromJson(
|
||||||
format(
|
format(
|
||||||
'[
|
'[
|
||||||
["{0}"],
|
["{0}"],
|
||||||
["x86_64", "x86_64-nonfree", "aarch64", "aarch64-nonfree", "riscv64", "raspberrypi"]
|
["x86_64", "x86_64-nonfree", "aarch64", "aarch64-nonfree", "riscv64"]
|
||||||
]',
|
]',
|
||||||
github.event.inputs.platform || 'ALL'
|
github.event.inputs.platform || 'ALL'
|
||||||
)
|
)
|
||||||
@@ -225,6 +189,10 @@ jobs:
|
|||||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY" # Pre-cached tool cache (Go, Node, etc.)
|
sudo rm -rf "$AGENT_TOOLSDIRECTORY" # Pre-cached tool cache (Go, Node, etc.)
|
||||||
if: ${{ github.event.inputs.runner != 'fast' }}
|
if: ${{ github.event.inputs.runner != 'fast' }}
|
||||||
|
|
||||||
|
# BuildJet runners lack /opt/hostedtoolcache, which setup-qemu expects
|
||||||
|
- name: Ensure hostedtoolcache exists
|
||||||
|
run: sudo mkdir -p /opt/hostedtoolcache && sudo chown $USER:$USER /opt/hostedtoolcache
|
||||||
|
|
||||||
- name: Set up docker QEMU
|
- name: Set up docker QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
|||||||
11
.github/workflows/test.yaml
vendored
11
.github/workflows/test.yaml
vendored
@@ -11,7 +11,7 @@ on:
|
|||||||
- next/*
|
- next/*
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.sha }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -21,15 +21,18 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Run Automated Tests
|
name: Run Automated Tests
|
||||||
|
if: github.event.pull_request.draft != true
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
- uses: ./.github/actions/setup-build
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODEJS_VERSION }}
|
nodejs-version: ${{ env.NODEJS_VERSION }}
|
||||||
|
free-space: "false"
|
||||||
|
setup-docker: "false"
|
||||||
|
setup-sccache: "false"
|
||||||
|
|
||||||
- name: Build And Run Tests
|
- name: Build And Run Tests
|
||||||
run: make test
|
run: make test
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ openssh-server
|
|||||||
podman
|
podman
|
||||||
psmisc
|
psmisc
|
||||||
qemu-guest-agent
|
qemu-guest-agent
|
||||||
|
qemu-user-static
|
||||||
rfkill
|
rfkill
|
||||||
rsync
|
rsync
|
||||||
samba-common-bin
|
samba-common-bin
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ cat << EOF
|
|||||||
# ISO Downloads
|
# ISO Downloads
|
||||||
|
|
||||||
- [x86_64/AMD64](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_x86_64-nonfree.iso))
|
- [x86_64/AMD64](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_x86_64-nonfree.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")
|
- [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](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_aarch64-nonfree.iso))
|
||||||
- [aarch64/ARM64 (Slim/FOSS-Only)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_aarch64.iso) "Without proprietary software or drivers")
|
- [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.iso))
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@@ -7817,7 +7817,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "start-os"
|
name = "start-os"
|
||||||
version = "0.4.0-alpha.18"
|
version = "0.4.0-alpha.19"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes 0.7.5",
|
"aes 0.7.5",
|
||||||
"arti-client",
|
"arti-client",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ license = "MIT"
|
|||||||
name = "start-os"
|
name = "start-os"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/Start9Labs/start-os"
|
repository = "https://github.com/Start9Labs/start-os"
|
||||||
version = "0.4.0-alpha.18" # VERSION_BUMP
|
version = "0.4.0-alpha.19" # VERSION_BUMP
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "startos"
|
name = "startos"
|
||||||
@@ -176,6 +176,7 @@ mio = "1"
|
|||||||
new_mime_guess = "4"
|
new_mime_guess = "4"
|
||||||
nix = { version = "0.30.1", features = [
|
nix = { version = "0.30.1", features = [
|
||||||
"fs",
|
"fs",
|
||||||
|
"hostname",
|
||||||
"mount",
|
"mount",
|
||||||
"net",
|
"net",
|
||||||
"process",
|
"process",
|
||||||
|
|||||||
@@ -1843,18 +1843,18 @@ service.mod.failed-to-parse-package-data-entry:
|
|||||||
pl_PL: "Nie udało się przeanalizować PackageDataEntry, znaleziono: %{error}"
|
pl_PL: "Nie udało się przeanalizować PackageDataEntry, znaleziono: %{error}"
|
||||||
|
|
||||||
service.mod.no-matching-subcontainers:
|
service.mod.no-matching-subcontainers:
|
||||||
en_US: "no matching subcontainers are running for %{id}; some possible choices are:\n%{subcontainers}"
|
en_US: "no matching subcontainers are running for %{id}; some possible choices are:"
|
||||||
de_DE: "keine passenden Subcontainer laufen für %{id}; einige mögliche Optionen sind:\n%{subcontainers}"
|
de_DE: "keine passenden Subcontainer laufen für %{id}; einige mögliche Optionen sind:"
|
||||||
es_ES: "no hay subcontenedores coincidentes ejecutándose para %{id}; algunas opciones posibles son:\n%{subcontainers}"
|
es_ES: "no hay subcontenedores coincidentes ejecutándose para %{id}; algunas opciones posibles son:"
|
||||||
fr_FR: "aucun sous-conteneur correspondant n'est en cours d'exécution pour %{id} ; voici quelques choix possibles :\n%{subcontainers}"
|
fr_FR: "aucun sous-conteneur correspondant n'est en cours d'exécution pour %{id} ; voici quelques choix possibles :"
|
||||||
pl_PL: "nie działają pasujące podkontenery dla %{id}; niektóre możliwe wybory to:\n%{subcontainers}"
|
pl_PL: "nie działają pasujące podkontenery dla %{id}; niektóre możliwe wybory to:"
|
||||||
|
|
||||||
service.mod.multiple-subcontainers-found:
|
service.mod.multiple-subcontainers-found:
|
||||||
en_US: "multiple subcontainers found for %{id}: \n%{subcontainer_ids}"
|
en_US: "multiple subcontainers found for %{id}"
|
||||||
de_DE: "mehrere Subcontainer für %{id} gefunden: \n%{subcontainer_ids}"
|
de_DE: "mehrere Subcontainer für %{id} gefunden"
|
||||||
es_ES: "se encontraron múltiples subcontenedores para %{id}: \n%{subcontainer_ids}"
|
es_ES: "se encontraron múltiples subcontenedores para %{id}"
|
||||||
fr_FR: "plusieurs sous-conteneurs trouvés pour %{id} : \n%{subcontainer_ids}"
|
fr_FR: "plusieurs sous-conteneurs trouvés pour %{id}"
|
||||||
pl_PL: "znaleziono wiele podkontenerów dla %{id}: \n%{subcontainer_ids}"
|
pl_PL: "znaleziono wiele podkontenerów dla %{id}"
|
||||||
|
|
||||||
service.mod.invalid-byte-length-for-signal:
|
service.mod.invalid-byte-length-for-signal:
|
||||||
en_US: "invalid byte length for signal: %{length}"
|
en_US: "invalid byte length for signal: %{length}"
|
||||||
@@ -3703,6 +3703,20 @@ help.arg.wireguard-config:
|
|||||||
fr_FR: "Configuration WireGuard"
|
fr_FR: "Configuration WireGuard"
|
||||||
pl_PL: "Konfiguracja WireGuard"
|
pl_PL: "Konfiguracja WireGuard"
|
||||||
|
|
||||||
|
help.s9pk-s3base:
|
||||||
|
en_US: "Base URL for publishing s9pks"
|
||||||
|
de_DE: "Basis-URL für die Veröffentlichung von s9pks"
|
||||||
|
es_ES: "URL base para publicar s9pks"
|
||||||
|
fr_FR: "URL de base pour publier les s9pks"
|
||||||
|
pl_PL: "Bazowy URL do publikowania s9pks"
|
||||||
|
|
||||||
|
help.s9pk-s3bucket:
|
||||||
|
en_US: "S3 bucket to publish s9pks to (should correspond to s3base)"
|
||||||
|
de_DE: "S3-Bucket zum Veröffentlichen von s9pks (sollte mit s3base übereinstimmen)"
|
||||||
|
es_ES: "Bucket S3 para publicar s9pks (debe corresponder con s3base)"
|
||||||
|
fr_FR: "Bucket S3 pour publier les s9pks (doit correspondre à s3base)"
|
||||||
|
pl_PL: "Bucket S3 do publikowania s9pks (powinien odpowiadać s3base)"
|
||||||
|
|
||||||
# CLI command descriptions (about.*)
|
# CLI command descriptions (about.*)
|
||||||
about.add-address-to-host:
|
about.add-address-to-host:
|
||||||
en_US: "Add an address to this host"
|
en_US: "Add an address to this host"
|
||||||
@@ -4866,6 +4880,13 @@ about.persist-new-notification:
|
|||||||
fr_FR: "Persister une nouvelle notification"
|
fr_FR: "Persister une nouvelle notification"
|
||||||
pl_PL: "Utrwal nowe powiadomienie"
|
pl_PL: "Utrwal nowe powiadomienie"
|
||||||
|
|
||||||
|
about.publish-s9pk:
|
||||||
|
en_US: "Publish s9pk to S3 bucket and index on registry"
|
||||||
|
de_DE: "S9pk in S3-Bucket veröffentlichen und in Registry indizieren"
|
||||||
|
es_ES: "Publicar s9pk en bucket S3 e indexar en el registro"
|
||||||
|
fr_FR: "Publier s9pk dans le bucket S3 et indexer dans le registre"
|
||||||
|
pl_PL: "Opublikuj s9pk do bucketu S3 i zindeksuj w rejestrze"
|
||||||
|
|
||||||
about.rebuild-service-container:
|
about.rebuild-service-container:
|
||||||
en_US: "Rebuild service container"
|
en_US: "Rebuild service container"
|
||||||
de_DE: "Dienst-Container neu erstellen"
|
de_DE: "Dienst-Container neu erstellen"
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ pub struct CliContextSeed {
|
|||||||
pub registry_url: Option<Url>,
|
pub registry_url: Option<Url>,
|
||||||
pub registry_hostname: Vec<InternedString>,
|
pub registry_hostname: Vec<InternedString>,
|
||||||
pub registry_listen: Option<SocketAddr>,
|
pub registry_listen: Option<SocketAddr>,
|
||||||
|
pub s9pk_s3base: Option<Url>,
|
||||||
|
pub s9pk_s3bucket: Option<InternedString>,
|
||||||
pub tunnel_addr: Option<SocketAddr>,
|
pub tunnel_addr: Option<SocketAddr>,
|
||||||
pub tunnel_listen: Option<SocketAddr>,
|
pub tunnel_listen: Option<SocketAddr>,
|
||||||
pub client: Client,
|
pub client: Client,
|
||||||
@@ -129,6 +131,8 @@ impl CliContext {
|
|||||||
.transpose()?,
|
.transpose()?,
|
||||||
registry_hostname: config.registry_hostname.unwrap_or_default(),
|
registry_hostname: config.registry_hostname.unwrap_or_default(),
|
||||||
registry_listen: config.registry_listen,
|
registry_listen: config.registry_listen,
|
||||||
|
s9pk_s3base: config.s9pk_s3base,
|
||||||
|
s9pk_s3bucket: config.s9pk_s3bucket,
|
||||||
tunnel_addr: config.tunnel,
|
tunnel_addr: config.tunnel,
|
||||||
tunnel_listen: config.tunnel_listen,
|
tunnel_listen: config.tunnel_listen,
|
||||||
client: {
|
client: {
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ pub struct ClientConfig {
|
|||||||
pub registry_hostname: Option<Vec<InternedString>>,
|
pub registry_hostname: Option<Vec<InternedString>>,
|
||||||
#[arg(skip)]
|
#[arg(skip)]
|
||||||
pub registry_listen: Option<SocketAddr>,
|
pub registry_listen: Option<SocketAddr>,
|
||||||
|
#[arg(long, help = "help.s9pk-s3base")]
|
||||||
|
pub s9pk_s3base: Option<Url>,
|
||||||
|
#[arg(long, help = "help.s9pk-s3bucket")]
|
||||||
|
pub s9pk_s3bucket: Option<InternedString>,
|
||||||
#[arg(short = 't', long, help = "help.arg.tunnel-address")]
|
#[arg(short = 't', long, help = "help.arg.tunnel-address")]
|
||||||
pub tunnel: Option<SocketAddr>,
|
pub tunnel: Option<SocketAddr>,
|
||||||
#[arg(skip)]
|
#[arg(skip)]
|
||||||
@@ -89,8 +93,13 @@ impl ContextConfig for ClientConfig {
|
|||||||
self.host = self.host.take().or(other.host);
|
self.host = self.host.take().or(other.host);
|
||||||
self.registry = self.registry.take().or(other.registry);
|
self.registry = self.registry.take().or(other.registry);
|
||||||
self.registry_hostname = self.registry_hostname.take().or(other.registry_hostname);
|
self.registry_hostname = self.registry_hostname.take().or(other.registry_hostname);
|
||||||
|
self.registry_listen = self.registry_listen.take().or(other.registry_listen);
|
||||||
|
self.s9pk_s3base = self.s9pk_s3base.take().or(other.s9pk_s3base);
|
||||||
|
self.s9pk_s3bucket = self.s9pk_s3bucket.take().or(other.s9pk_s3bucket);
|
||||||
self.tunnel = self.tunnel.take().or(other.tunnel);
|
self.tunnel = self.tunnel.take().or(other.tunnel);
|
||||||
|
self.tunnel_listen = self.tunnel_listen.take().or(other.tunnel_listen);
|
||||||
self.proxy = self.proxy.take().or(other.proxy);
|
self.proxy = self.proxy.take().or(other.proxy);
|
||||||
|
self.socks_listen = self.socks_listen.take().or(other.socks_listen);
|
||||||
self.cookie_path = self.cookie_path.take().or(other.cookie_path);
|
self.cookie_path = self.cookie_path.take().or(other.cookie_path);
|
||||||
self.developer_key_path = self.developer_key_path.take().or(other.developer_key_path);
|
self.developer_key_path = self.developer_key_path.take().or(other.developer_key_path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -579,6 +579,7 @@ impl RpcContext {
|
|||||||
pub async fn call_remote<RemoteContext>(
|
pub async fn call_remote<RemoteContext>(
|
||||||
&self,
|
&self,
|
||||||
method: &str,
|
method: &str,
|
||||||
|
metadata: OrdMap<&'static str, Value>,
|
||||||
params: Value,
|
params: Value,
|
||||||
) -> Result<Value, RpcError>
|
) -> Result<Value, RpcError>
|
||||||
where
|
where
|
||||||
@@ -587,7 +588,7 @@ impl RpcContext {
|
|||||||
<Self as CallRemote<RemoteContext, Empty>>::call_remote(
|
<Self as CallRemote<RemoteContext, Empty>>::call_remote(
|
||||||
&self,
|
&self,
|
||||||
method,
|
method,
|
||||||
OrdMap::new(),
|
metadata,
|
||||||
params,
|
params,
|
||||||
Empty {},
|
Empty {},
|
||||||
)
|
)
|
||||||
@@ -596,20 +597,15 @@ impl RpcContext {
|
|||||||
pub async fn call_remote_with<RemoteContext, T>(
|
pub async fn call_remote_with<RemoteContext, T>(
|
||||||
&self,
|
&self,
|
||||||
method: &str,
|
method: &str,
|
||||||
|
metadata: OrdMap<&'static str, Value>,
|
||||||
params: Value,
|
params: Value,
|
||||||
extra: T,
|
extra: T,
|
||||||
) -> Result<Value, RpcError>
|
) -> Result<Value, RpcError>
|
||||||
where
|
where
|
||||||
Self: CallRemote<RemoteContext, T>,
|
Self: CallRemote<RemoteContext, T>,
|
||||||
{
|
{
|
||||||
<Self as CallRemote<RemoteContext, T>>::call_remote(
|
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, metadata, params, extra)
|
||||||
&self,
|
.await
|
||||||
method,
|
|
||||||
OrdMap::new(),
|
|
||||||
params,
|
|
||||||
extra,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl AsRef<Client> for RpcContext {
|
impl AsRef<Client> for RpcContext {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use axum::http::StatusCode;
|
|||||||
use axum::http::uri::InvalidUri;
|
use axum::http::uri::InvalidUri;
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use num_enum::TryFromPrimitive;
|
use num_enum::TryFromPrimitive;
|
||||||
use patch_db::Revision;
|
use patch_db::Value;
|
||||||
use rpc_toolkit::reqwest;
|
use rpc_toolkit::reqwest;
|
||||||
use rpc_toolkit::yajrc::{
|
use rpc_toolkit::yajrc::{
|
||||||
INVALID_PARAMS_ERROR, INVALID_REQUEST_ERROR, METHOD_NOT_FOUND_ERROR, PARSE_ERROR, RpcError,
|
INVALID_PARAMS_ERROR, INVALID_REQUEST_ERROR, METHOD_NOT_FOUND_ERROR, PARSE_ERROR, RpcError,
|
||||||
@@ -16,6 +16,7 @@ use tokio_rustls::rustls;
|
|||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::InvalidId;
|
use crate::InvalidId;
|
||||||
|
use crate::prelude::to_value;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
|
||||||
#[repr(i32)]
|
#[repr(i32)]
|
||||||
@@ -197,7 +198,7 @@ pub struct Error {
|
|||||||
pub source: color_eyre::eyre::Error,
|
pub source: color_eyre::eyre::Error,
|
||||||
pub debug: Option<color_eyre::eyre::Error>,
|
pub debug: Option<color_eyre::eyre::Error>,
|
||||||
pub kind: ErrorKind,
|
pub kind: ErrorKind,
|
||||||
pub revision: Option<Revision>,
|
pub info: Value,
|
||||||
pub task: Option<JoinHandle<()>>,
|
pub task: Option<JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +229,7 @@ impl Error {
|
|||||||
source: source.into(),
|
source: source.into(),
|
||||||
debug,
|
debug,
|
||||||
kind,
|
kind,
|
||||||
revision: None,
|
info: Value::Null,
|
||||||
task: None,
|
task: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,7 +238,7 @@ impl Error {
|
|||||||
source: eyre!("{}", self.source),
|
source: eyre!("{}", self.source),
|
||||||
debug: self.debug.as_ref().map(|e| eyre!("{e}")),
|
debug: self.debug.as_ref().map(|e| eyre!("{e}")),
|
||||||
kind: self.kind,
|
kind: self.kind,
|
||||||
revision: self.revision.clone(),
|
info: self.info.clone(),
|
||||||
task: None,
|
task: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,6 +246,10 @@ impl Error {
|
|||||||
self.task = Some(task);
|
self.task = Some(task);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
pub fn with_info(mut self, info: Value) -> Self {
|
||||||
|
self.info = info;
|
||||||
|
self
|
||||||
|
}
|
||||||
pub async fn wait(mut self) -> Self {
|
pub async fn wait(mut self) -> Self {
|
||||||
if let Some(task) = &mut self.task {
|
if let Some(task) = &mut self.task {
|
||||||
task.await.log_err();
|
task.await.log_err();
|
||||||
@@ -423,6 +428,8 @@ impl From<patch_db::value::Error> for Error {
|
|||||||
pub struct ErrorData {
|
pub struct ErrorData {
|
||||||
pub details: String,
|
pub details: String,
|
||||||
pub debug: String,
|
pub debug: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub info: Value,
|
||||||
}
|
}
|
||||||
impl Display for ErrorData {
|
impl Display for ErrorData {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
@@ -440,6 +447,7 @@ impl From<Error> for ErrorData {
|
|||||||
Self {
|
Self {
|
||||||
details: value.to_string(),
|
details: value.to_string(),
|
||||||
debug: format!("{:?}", value),
|
debug: format!("{:?}", value),
|
||||||
|
info: value.info,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -470,40 +478,31 @@ impl From<&RpcError> for ErrorData {
|
|||||||
.or_else(|| d.as_str().map(|s| s.to_owned()))
|
.or_else(|| d.as_str().map(|s| s.to_owned()))
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| value.message.clone().into_owned()),
|
.unwrap_or_else(|| value.message.clone().into_owned()),
|
||||||
|
info: to_value(
|
||||||
|
&value
|
||||||
|
.data
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|d| d.as_object().and_then(|d| d.get("info"))),
|
||||||
|
)
|
||||||
|
.unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Error> for RpcError {
|
impl From<Error> for RpcError {
|
||||||
fn from(e: Error) -> Self {
|
fn from(e: Error) -> Self {
|
||||||
let mut data_object = serde_json::Map::with_capacity(3);
|
let kind = e.kind;
|
||||||
data_object.insert("details".to_owned(), format!("{}", e.source).into());
|
let data = ErrorData::from(e);
|
||||||
data_object.insert("debug".to_owned(), format!("{:?}", e.source).into());
|
RpcError {
|
||||||
data_object.insert(
|
code: kind as i32,
|
||||||
"revision".to_owned(),
|
message: kind.as_str().into(),
|
||||||
match serde_json::to_value(&e.revision) {
|
data: Some(match serde_json::to_value(&data) {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("Error serializing revision for Error object: {}", e);
|
tracing::warn!("Error serializing ErrorData object: {}", e);
|
||||||
serde_json::Value::Null
|
serde_json::Value::Null
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
);
|
|
||||||
RpcError {
|
|
||||||
code: e.kind as i32,
|
|
||||||
message: e.kind.as_str().into(),
|
|
||||||
data: Some(
|
|
||||||
match serde_json::to_value(&ErrorData {
|
|
||||||
details: format!("{}", e.source),
|
|
||||||
debug: format!("{:?}", e.source),
|
|
||||||
}) {
|
|
||||||
Ok(a) => a,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!("Error serializing revision for Error object: {}", e);
|
|
||||||
serde_json::Value::Null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -606,7 +605,7 @@ where
|
|||||||
kind,
|
kind,
|
||||||
source,
|
source,
|
||||||
debug,
|
debug,
|
||||||
revision: None,
|
info: Value::Null,
|
||||||
task: None,
|
task: None,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ pub async fn install(
|
|||||||
let package: GetPackageResponse = from_value(
|
let package: GetPackageResponse = from_value(
|
||||||
ctx.call_remote_with::<RegistryContext, _>(
|
ctx.call_remote_with::<RegistryContext, _>(
|
||||||
"package.get",
|
"package.get",
|
||||||
|
[("get_device_info", Value::Bool(true))]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
json!({
|
json!({
|
||||||
"id": id,
|
"id": id,
|
||||||
"targetVersion": VersionRange::exactly(version.deref().clone()),
|
"targetVersion": VersionRange::exactly(version.deref().clone()),
|
||||||
|
|||||||
@@ -540,7 +540,10 @@ pub fn package<C: Context>() -> ParentHandler<C> {
|
|||||||
.with_about("about.execute-commands-container")
|
.with_about("about.execute-commands-container")
|
||||||
.no_cli(),
|
.no_cli(),
|
||||||
)
|
)
|
||||||
.subcommand("attach", from_fn_async(service::cli_attach).no_display())
|
.subcommand(
|
||||||
|
"attach",
|
||||||
|
from_fn_async_local(service::cli_attach).no_display(),
|
||||||
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"host",
|
"host",
|
||||||
net::host::host_api::<C>().with_about("about.manage-network-hosts-package"),
|
net::host::host_api::<C>().with_about("about.manage-network-hosts-package"),
|
||||||
|
|||||||
@@ -135,20 +135,24 @@ pub struct CliAddPackageParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cli_add_package(
|
pub async fn cli_add_package(
|
||||||
HandlerArgs {
|
ctx: CliContext,
|
||||||
context: ctx,
|
CliAddPackageParams {
|
||||||
parent_method,
|
file,
|
||||||
method,
|
url,
|
||||||
params:
|
no_verify,
|
||||||
CliAddPackageParams {
|
}: CliAddPackageParams,
|
||||||
file,
|
|
||||||
url,
|
|
||||||
no_verify,
|
|
||||||
},
|
|
||||||
..
|
|
||||||
}: HandlerArgs<CliContext, CliAddPackageParams>,
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let s9pk = S9pk::open(&file, None).await?;
|
let s9pk = S9pk::open(&file, None).await?;
|
||||||
|
cli_add_package_impl(ctx, s9pk, url, no_verify).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cli_add_package_impl(
|
||||||
|
ctx: CliContext,
|
||||||
|
s9pk: S9pk,
|
||||||
|
url: Vec<Url>,
|
||||||
|
no_verify: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let manifest = s9pk.as_manifest();
|
||||||
|
|
||||||
let progress = FullProgressTracker::new();
|
let progress = FullProgressTracker::new();
|
||||||
let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(1));
|
let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(1));
|
||||||
@@ -170,8 +174,16 @@ pub async fn cli_add_package(
|
|||||||
Some(1),
|
Some(1),
|
||||||
);
|
);
|
||||||
|
|
||||||
let progress_task =
|
let progress_task = progress.progress_bar_task(&format!(
|
||||||
progress.progress_bar_task(&format!("Adding {} to registry...", file.display()));
|
"Adding {}@{}{} to registry...",
|
||||||
|
manifest.id,
|
||||||
|
manifest.version,
|
||||||
|
manifest
|
||||||
|
.hardware_requirements
|
||||||
|
.arch
|
||||||
|
.as_ref()
|
||||||
|
.map_or(String::new(), |a| format!(" ({})", a.iter().join("/")))
|
||||||
|
));
|
||||||
|
|
||||||
sign_phase.start();
|
sign_phase.start();
|
||||||
let commitment = s9pk.as_archive().commitment().await?;
|
let commitment = s9pk.as_archive().commitment().await?;
|
||||||
@@ -188,7 +200,7 @@ pub async fn cli_add_package(
|
|||||||
|
|
||||||
index_phase.start();
|
index_phase.start();
|
||||||
ctx.call_remote::<RegistryContext>(
|
ctx.call_remote::<RegistryContext>(
|
||||||
&parent_method.into_iter().chain(method).join("."),
|
"package.add",
|
||||||
imbl_value::json!({
|
imbl_value::json!({
|
||||||
"urls": &url,
|
"urls": &url,
|
||||||
"signature": AnySignature::Ed25519(signature),
|
"signature": AnySignature::Ed25519(signature),
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
use std::ops::Deref;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use rpc_toolkit::{Empty, HandlerExt, ParentHandler, from_fn_async};
|
use rpc_toolkit::{Empty, HandlerExt, ParentHandler, from_fn_async};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::process::Command;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::ImageId;
|
use crate::ImageId;
|
||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
@@ -13,9 +16,9 @@ use crate::s9pk::manifest::Manifest;
|
|||||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||||
use crate::s9pk::v2::SIG_CONTEXT;
|
use crate::s9pk::v2::SIG_CONTEXT;
|
||||||
use crate::s9pk::v2::pack::ImageConfig;
|
use crate::s9pk::v2::pack::ImageConfig;
|
||||||
use crate::util::Apply;
|
|
||||||
use crate::util::io::{TmpDir, create_file, open_file};
|
use crate::util::io::{TmpDir, create_file, open_file};
|
||||||
use crate::util::serde::{HandlerExtSerde, apply_expr};
|
use crate::util::serde::{HandlerExtSerde, apply_expr};
|
||||||
|
use crate::util::{Apply, Invoke};
|
||||||
|
|
||||||
pub const SKIP_ENV: &[&str] = &["TERM", "container", "HOME", "HOSTNAME"];
|
pub const SKIP_ENV: &[&str] = &["TERM", "container", "HOME", "HOSTNAME"];
|
||||||
|
|
||||||
@@ -61,6 +64,12 @@ pub fn s9pk() -> ParentHandler<CliContext> {
|
|||||||
.no_display()
|
.no_display()
|
||||||
.with_about("about.convert-s9pk-v1-to-v2"),
|
.with_about("about.convert-s9pk-v1-to-v2"),
|
||||||
)
|
)
|
||||||
|
.subcommand(
|
||||||
|
"publish",
|
||||||
|
from_fn_async(publish)
|
||||||
|
.no_display()
|
||||||
|
.with_about("about.publish-s9pk"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Parser)]
|
#[derive(Deserialize, Serialize, Parser)]
|
||||||
@@ -256,3 +265,61 @@ async fn convert(ctx: CliContext, S9pkPath { s9pk: s9pk_path }: S9pkPath) -> Res
|
|||||||
tokio::fs::rename(tmp_path, s9pk_path).await?;
|
tokio::fs::rename(tmp_path, s9pk_path).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn publish(ctx: CliContext, S9pkPath { s9pk: s9pk_path }: S9pkPath) -> Result<(), Error> {
|
||||||
|
let filename = s9pk_path.file_name().unwrap().to_string_lossy();
|
||||||
|
let s9pk = super::S9pk::open(&s9pk_path, None).await?;
|
||||||
|
let manifest = s9pk.as_manifest();
|
||||||
|
let path = [
|
||||||
|
manifest.id.deref(),
|
||||||
|
manifest.version.as_str(),
|
||||||
|
filename.deref(),
|
||||||
|
];
|
||||||
|
let mut s3url = ctx
|
||||||
|
.s9pk_s3base
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| Error::new(eyre!("--s9pk-s3base required"), ErrorKind::InvalidRequest))?
|
||||||
|
.clone();
|
||||||
|
s3url
|
||||||
|
.path_segments_mut()
|
||||||
|
.map_err(|_| {
|
||||||
|
Error::new(
|
||||||
|
eyre!("s9pk-s3base is invalid (missing protocol?)"),
|
||||||
|
ErrorKind::ParseUrl,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.pop_if_empty()
|
||||||
|
.extend(path);
|
||||||
|
|
||||||
|
let mut s3dest = format!(
|
||||||
|
"s3://{}",
|
||||||
|
ctx.s9pk_s3bucket
|
||||||
|
.as_deref()
|
||||||
|
.or_else(|| s3url
|
||||||
|
.host_str()
|
||||||
|
.and_then(|h| h.split_once(".").map(|h| h.0)))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::new(eyre!("--s9pk-s3bucket required"), ErrorKind::InvalidRequest)
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
.parse::<Url>()?;
|
||||||
|
s3dest
|
||||||
|
.path_segments_mut()
|
||||||
|
.map_err(|_| {
|
||||||
|
Error::new(
|
||||||
|
eyre!("s9pk-s3base is invalid (missing protocol?)"),
|
||||||
|
ErrorKind::ParseUrl,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.pop_if_empty()
|
||||||
|
.extend(path);
|
||||||
|
Command::new("s3cmd")
|
||||||
|
.arg("put")
|
||||||
|
.arg("-P")
|
||||||
|
.arg(s9pk_path)
|
||||||
|
.arg(s3dest.as_str())
|
||||||
|
.capture(false)
|
||||||
|
.invoke(ErrorKind::Network)
|
||||||
|
.await?;
|
||||||
|
crate::registry::package::add::cli_add_package_impl(ctx, s9pk, vec![s3url], false).await
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use clap::Parser;
|
|||||||
use futures::future::{BoxFuture, ready};
|
use futures::future::{BoxFuture, ready};
|
||||||
use futures::{FutureExt, TryStreamExt};
|
use futures::{FutureExt, TryStreamExt};
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
|
use itertools::Itertools;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
@@ -686,7 +687,7 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> {
|
|||||||
let manifest = s9pk.as_manifest_mut();
|
let manifest = s9pk.as_manifest_mut();
|
||||||
manifest.git_hash = Some(GitHash::from_path(params.path()).await?);
|
manifest.git_hash = Some(GitHash::from_path(params.path()).await?);
|
||||||
if !params.arch.is_empty() {
|
if !params.arch.is_empty() {
|
||||||
let arches = match manifest.hardware_requirements.arch.take() {
|
let arches: BTreeSet<InternedString> = match manifest.hardware_requirements.arch.take() {
|
||||||
Some(a) => params
|
Some(a) => params
|
||||||
.arch
|
.arch
|
||||||
.iter()
|
.iter()
|
||||||
@@ -695,10 +696,41 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> {
|
|||||||
.collect(),
|
.collect(),
|
||||||
None => params.arch.iter().cloned().collect(),
|
None => params.arch.iter().cloned().collect(),
|
||||||
};
|
};
|
||||||
manifest
|
if arches.is_empty() {
|
||||||
.images
|
return Err(Error::new(
|
||||||
.values_mut()
|
eyre!(
|
||||||
.for_each(|c| c.arch = c.arch.intersection(&arches).cloned().collect());
|
"none of the requested architectures ({:?}) are supported by this package",
|
||||||
|
params.arch
|
||||||
|
),
|
||||||
|
ErrorKind::InvalidRequest,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
manifest.images.iter_mut().for_each(|(id, c)| {
|
||||||
|
let filtered = c
|
||||||
|
.arch
|
||||||
|
.intersection(&arches)
|
||||||
|
.cloned()
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
if filtered.is_empty() {
|
||||||
|
if let Some(arch) = &c.emulate_missing_as {
|
||||||
|
tracing::warn!(
|
||||||
|
"ImageId {} is not available for {}, emulating as {}",
|
||||||
|
id,
|
||||||
|
arches.iter().join("/"),
|
||||||
|
arch
|
||||||
|
);
|
||||||
|
c.arch = [arch.clone()].into_iter().collect();
|
||||||
|
} else {
|
||||||
|
tracing::error!(
|
||||||
|
"ImageId {} is not available for {}",
|
||||||
|
id,
|
||||||
|
arches.iter().join("/"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.arch = filtered;
|
||||||
|
}
|
||||||
|
});
|
||||||
manifest.hardware_requirements.arch = Some(arches);
|
manifest.hardware_requirements.arch = Some(arches);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use crate::rpc_continuations::Guid;
|
|||||||
use crate::service::effects::prelude::*;
|
use crate::service::effects::prelude::*;
|
||||||
use crate::service::persistent_container::Subcontainer;
|
use crate::service::persistent_container::Subcontainer;
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
|
use crate::util::io::write_file_owned_atomic;
|
||||||
|
|
||||||
pub const NVIDIA_OVERLAY_PATH: &str = "/var/tmp/startos/nvidia-overlay";
|
pub const NVIDIA_OVERLAY_PATH: &str = "/var/tmp/startos/nvidia-overlay";
|
||||||
pub const NVIDIA_OVERLAY_DEBIAN: &str = "/var/tmp/startos/nvidia-overlay/debian";
|
pub const NVIDIA_OVERLAY_DEBIAN: &str = "/var/tmp/startos/nvidia-overlay/debian";
|
||||||
@@ -94,7 +95,7 @@ pub async fn create_subcontainer_fs(
|
|||||||
.cloned()
|
.cloned()
|
||||||
{
|
{
|
||||||
let guid = Guid::new();
|
let guid = Guid::new();
|
||||||
let rootfs_dir = context
|
let lxc_container = context
|
||||||
.seed
|
.seed
|
||||||
.persistent_container
|
.persistent_container
|
||||||
.lxc_container
|
.lxc_container
|
||||||
@@ -104,8 +105,9 @@ pub async fn create_subcontainer_fs(
|
|||||||
eyre!("PersistentContainer has been destroyed"),
|
eyre!("PersistentContainer has been destroyed"),
|
||||||
ErrorKind::Incoherent,
|
ErrorKind::Incoherent,
|
||||||
)
|
)
|
||||||
})?
|
})?;
|
||||||
.rootfs_dir();
|
let container_guid = &lxc_container.guid;
|
||||||
|
let rootfs_dir = lxc_container.rootfs_dir();
|
||||||
let mountpoint = rootfs_dir
|
let mountpoint = rootfs_dir
|
||||||
.join("media/startos/subcontainers")
|
.join("media/startos/subcontainers")
|
||||||
.join(guid.as_ref());
|
.join(guid.as_ref());
|
||||||
@@ -154,6 +156,20 @@ pub async fn create_subcontainer_fs(
|
|||||||
.arg(&mountpoint)
|
.arg(&mountpoint)
|
||||||
.invoke(ErrorKind::Filesystem)
|
.invoke(ErrorKind::Filesystem)
|
||||||
.await?;
|
.await?;
|
||||||
|
write_file_owned_atomic(
|
||||||
|
mountpoint.join("etc/hostname"),
|
||||||
|
format!("{container_guid}\n"),
|
||||||
|
100000,
|
||||||
|
100000,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
write_file_owned_atomic(
|
||||||
|
mountpoint.join("etc/hosts"),
|
||||||
|
format!("127.0.0.1\tlocalhost\n127.0.1.1\t{container_guid}\n::1\tlocalhost ip6-localhost ip6-loopback\n"),
|
||||||
|
100000,
|
||||||
|
100000,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
tracing::info!("Mounted overlay {guid} for {image_id}");
|
tracing::info!("Mounted overlay {guid} for {image_id}");
|
||||||
context
|
context
|
||||||
.seed
|
.seed
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::collections::BTreeMap;
|
|
||||||
use std::ffi::{OsStr, OsString, c_int};
|
use std::ffi::{OsStr, OsString, c_int};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{IsTerminal, Read};
|
use std::io::{BufRead, BufReader, IsTerminal, Read};
|
||||||
use std::os::unix::process::{CommandExt, ExitStatusExt};
|
use std::os::unix::process::{CommandExt, ExitStatusExt};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command as StdCommand, Stdio};
|
use std::process::{Command as StdCommand, Stdio};
|
||||||
@@ -146,95 +145,160 @@ impl ExecParams {
|
|||||||
|
|
||||||
let mut cmd = StdCommand::new(command);
|
let mut cmd = StdCommand::new(command);
|
||||||
|
|
||||||
let passwd = std::fs::read_to_string(chroot.join("etc/passwd"))
|
let mut uid = Err(None);
|
||||||
.with_ctx(|_| (ErrorKind::Filesystem, "read /etc/passwd"))
|
let mut gid = Err(None);
|
||||||
.log_err()
|
let mut needs_home = true;
|
||||||
.unwrap_or_default();
|
|
||||||
let mut home = None;
|
|
||||||
|
|
||||||
if let Some((uid, gid)) =
|
if let Some(user) = user {
|
||||||
if let Some(uid) = user.as_deref().and_then(|u| u.parse::<u32>().ok()) {
|
if let Some((u, g)) = user.split_once(":") {
|
||||||
Some((uid, uid))
|
uid = Err(Some(u));
|
||||||
} else if let Some((uid, gid)) = user
|
gid = Err(Some(g));
|
||||||
.as_deref()
|
|
||||||
.and_then(|u| u.split_once(":"))
|
|
||||||
.and_then(|(u, g)| Some((u.parse::<u32>().ok()?, g.parse::<u32>().ok()?)))
|
|
||||||
{
|
|
||||||
Some((uid, gid))
|
|
||||||
} else if let Some(user) = user {
|
|
||||||
Some(
|
|
||||||
if let Some((uid, gid)) = passwd.lines().find_map(|l| {
|
|
||||||
let l = l.trim();
|
|
||||||
let mut split = l.split(":");
|
|
||||||
if user != split.next()? {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
split.next(); // throw away x
|
|
||||||
let uid = split.next()?.parse().ok()?;
|
|
||||||
let gid = split.next()?.parse().ok()?;
|
|
||||||
split.next(); // throw away group name
|
|
||||||
|
|
||||||
home = split.next();
|
|
||||||
|
|
||||||
Some((uid, gid))
|
|
||||||
// uid gid
|
|
||||||
}) {
|
|
||||||
(uid, gid)
|
|
||||||
} else if user == "root" {
|
|
||||||
(0, 0)
|
|
||||||
} else {
|
|
||||||
None.or_not_found(lazy_format!("{user} in /etc/passwd"))?
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
uid = Err(Some(user));
|
||||||
}
|
}
|
||||||
{
|
|
||||||
if home.is_none() {
|
|
||||||
home = passwd.lines().find_map(|l| {
|
|
||||||
let l = l.trim();
|
|
||||||
let mut split = l.split(":");
|
|
||||||
|
|
||||||
split.next(); // throw away user name
|
|
||||||
split.next(); // throw away x
|
|
||||||
if split.next()?.parse::<u32>().ok()? != uid {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
split.next(); // throw away gid
|
|
||||||
split.next(); // throw away group name
|
|
||||||
|
|
||||||
split.next()
|
|
||||||
})
|
|
||||||
};
|
|
||||||
std::os::unix::fs::chown("/proc/self/fd/0", Some(uid), Some(gid)).ok();
|
|
||||||
std::os::unix::fs::chown("/proc/self/fd/1", Some(uid), Some(gid)).ok();
|
|
||||||
std::os::unix::fs::chown("/proc/self/fd/2", Some(uid), Some(gid)).ok();
|
|
||||||
cmd.uid(uid);
|
|
||||||
cmd.gid(gid);
|
|
||||||
} else {
|
|
||||||
home = Some("/root");
|
|
||||||
}
|
}
|
||||||
cmd.env("HOME", home.unwrap_or("/"));
|
|
||||||
|
|
||||||
let env_string = if let Some(env_file) = &env_file {
|
if let Some(u) = uid.err().flatten().and_then(|u| u.parse::<u32>().ok()) {
|
||||||
std::fs::read_to_string(env_file)
|
uid = Ok(u);
|
||||||
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("read {env:?}")))?
|
}
|
||||||
} else {
|
if let Some(g) = gid.err().flatten().and_then(|g| g.parse::<u32>().ok()) {
|
||||||
Default::default()
|
gid = Ok(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut update_env = |line: &str| {
|
||||||
|
if let Some((k, v)) = line.split_once("=") {
|
||||||
|
needs_home &= k != "HOME";
|
||||||
|
cmd.env(k, v);
|
||||||
|
} else {
|
||||||
|
tracing::warn!("Invalid line in env: {line}");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let env = env_string
|
if let Some(f) = env_file {
|
||||||
.lines()
|
let mut lines = BufReader::new(
|
||||||
.chain(env.iter().map(|l| l.as_str()))
|
File::open(&f).with_ctx(|_| (ErrorKind::Filesystem, format!("open r {f:?}")))?,
|
||||||
.map(|l| l.trim())
|
)
|
||||||
.filter_map(|l| l.split_once("="))
|
.lines();
|
||||||
.collect::<BTreeMap<_, _>>();
|
while let Some(line) = lines.next().transpose()? {
|
||||||
|
update_env(&line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for line in env {
|
||||||
|
update_env(&line);
|
||||||
|
}
|
||||||
|
|
||||||
|
let needs_gid = Err(None) == gid;
|
||||||
|
let mut username = InternedString::intern("root");
|
||||||
|
let mut handle_passwd_line = |line: &str| -> Option<()> {
|
||||||
|
let l = line.trim();
|
||||||
|
let mut split = l.split(":");
|
||||||
|
let user = split.next()?;
|
||||||
|
match uid {
|
||||||
|
Err(Some(u)) if u != user => return None,
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
split.next(); // throw away x
|
||||||
|
let u: u32 = split.next()?.parse().ok()?;
|
||||||
|
match uid {
|
||||||
|
Err(Some(_)) => uid = Ok(u),
|
||||||
|
Err(None) if u == 0 => uid = Ok(u),
|
||||||
|
Ok(uid) if uid != u => return None,
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
username = user.into();
|
||||||
|
|
||||||
|
if !needs_gid && !needs_home {
|
||||||
|
return Some(());
|
||||||
|
}
|
||||||
|
let g = split.next()?;
|
||||||
|
if needs_gid {
|
||||||
|
gid = Ok(g.parse().ok()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
if needs_home {
|
||||||
|
split.next(); // throw away group name
|
||||||
|
|
||||||
|
let home = split.next()?;
|
||||||
|
|
||||||
|
cmd.env("HOME", home);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut lines = BufReader::new(
|
||||||
|
File::open(chroot.join("etc/passwd"))
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, format!("open r /etc/passwd")))?,
|
||||||
|
)
|
||||||
|
.lines();
|
||||||
|
while let Some(line) = lines.next().transpose()? {
|
||||||
|
if handle_passwd_line(&line).is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut groups = Vec::new();
|
||||||
|
let mut handle_group_line = |line: &str| -> Option<()> {
|
||||||
|
let l = line.trim();
|
||||||
|
let mut split = l.split(":");
|
||||||
|
let name = split.next()?;
|
||||||
|
split.next()?; // throw away x
|
||||||
|
let g = split.next()?.parse::<u32>().ok()?;
|
||||||
|
match gid {
|
||||||
|
Err(Some(n)) if n == name => gid = Ok(g),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
let users = split.next()?;
|
||||||
|
if users.split(",").any(|u| u == &*username) {
|
||||||
|
groups.push(nix::unistd::Gid::from_raw(g));
|
||||||
|
}
|
||||||
|
Some(())
|
||||||
|
};
|
||||||
|
let mut lines = BufReader::new(
|
||||||
|
File::open(chroot.join("etc/group"))
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, format!("open r /etc/group")))?,
|
||||||
|
)
|
||||||
|
.lines();
|
||||||
|
while let Some(line) = lines.next().transpose()? {
|
||||||
|
if handle_group_line(&line).is_none() {
|
||||||
|
tracing::warn!("Invalid /etc/group line: {line}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::os::unix::fs::chroot(chroot)
|
std::os::unix::fs::chroot(chroot)
|
||||||
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("chroot {chroot:?}")))?;
|
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("chroot {chroot:?}")))?;
|
||||||
cmd.args(args);
|
if let Ok(uid) = uid {
|
||||||
for (k, v) in env {
|
if uid != 0 {
|
||||||
cmd.env(k, v);
|
std::os::unix::fs::chown("/proc/self/fd/0", Some(uid), gid.ok()).ok();
|
||||||
|
std::os::unix::fs::chown("/proc/self/fd/1", Some(uid), gid.ok()).ok();
|
||||||
|
std::os::unix::fs::chown("/proc/self/fd/2", Some(uid), gid.ok()).ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Handle credential changes in pre_exec to control the order:
|
||||||
|
// setgroups must happen before setgid/setuid (requires CAP_SETGID)
|
||||||
|
{
|
||||||
|
let set_uid = uid.ok();
|
||||||
|
let set_gid = gid.ok();
|
||||||
|
unsafe {
|
||||||
|
cmd.pre_exec(move || {
|
||||||
|
if !groups.is_empty() {
|
||||||
|
nix::unistd::setgroups(&groups)
|
||||||
|
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
|
||||||
|
}
|
||||||
|
if let Some(gid) = set_gid {
|
||||||
|
nix::unistd::setgid(nix::unistd::Gid::from_raw(gid))
|
||||||
|
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
|
||||||
|
}
|
||||||
|
if let Some(uid) = set_uid {
|
||||||
|
nix::unistd::setuid(nix::unistd::Uid::from_raw(uid))
|
||||||
|
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd.args(args);
|
||||||
|
|
||||||
if let Some(workdir) = workdir {
|
if let Some(workdir) = workdir {
|
||||||
cmd.current_dir(workdir);
|
cmd.current_dir(workdir);
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ use crate::util::io::{AsyncReadStream, AtomicFile, TermSize, delete_file};
|
|||||||
use crate::util::net::WebSocket;
|
use crate::util::net::WebSocket;
|
||||||
use crate::util::serde::Pem;
|
use crate::util::serde::Pem;
|
||||||
use crate::util::sync::SyncMutex;
|
use crate::util::sync::SyncMutex;
|
||||||
|
use crate::util::tui::choose;
|
||||||
use crate::volume::data_dir;
|
use crate::volume::data_dir;
|
||||||
use crate::{ActionId, CAP_1_KiB, DATA_DIR, HostId, ImageId, PackageId};
|
use crate::{ActionId, CAP_1_KiB, DATA_DIR, HostId, ImageId, PackageId};
|
||||||
|
|
||||||
@@ -709,6 +710,19 @@ pub async fn rebuild(ctx: RpcContext, RebuildParams { id }: RebuildParams) -> Re
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct SubcontainerInfo {
|
||||||
|
pub id: Guid,
|
||||||
|
pub name: InternedString,
|
||||||
|
pub image_id: ImageId,
|
||||||
|
}
|
||||||
|
impl std::fmt::Display for SubcontainerInfo {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let SubcontainerInfo { id, name, image_id } = self;
|
||||||
|
write!(f, "{id} => Name: {name}; Image: {image_id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, TS)]
|
#[derive(Deserialize, Serialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AttachParams {
|
pub struct AttachParams {
|
||||||
@@ -722,7 +736,7 @@ pub struct AttachParams {
|
|||||||
#[serde(rename = "__Auth_session")]
|
#[serde(rename = "__Auth_session")]
|
||||||
session: Option<InternedString>,
|
session: Option<InternedString>,
|
||||||
#[ts(type = "string | null")]
|
#[ts(type = "string | null")]
|
||||||
subcontainer: Option<InternedString>,
|
subcontainer: Option<Guid>,
|
||||||
#[ts(type = "string | null")]
|
#[ts(type = "string | null")]
|
||||||
name: Option<InternedString>,
|
name: Option<InternedString>,
|
||||||
#[ts(type = "string | null")]
|
#[ts(type = "string | null")]
|
||||||
@@ -745,7 +759,7 @@ pub async fn attach(
|
|||||||
user,
|
user,
|
||||||
}: AttachParams,
|
}: AttachParams,
|
||||||
) -> Result<Guid, Error> {
|
) -> Result<Guid, Error> {
|
||||||
let (container_id, subcontainer_id, image_id, workdir, root_command) = {
|
let (container_id, subcontainer_id, image_id, user, workdir, root_command) = {
|
||||||
let id = &id;
|
let id = &id;
|
||||||
|
|
||||||
let service = ctx.services.get(id).await;
|
let service = ctx.services.get(id).await;
|
||||||
@@ -786,13 +800,6 @@ pub async fn attach(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let format_subcontainer_pair = |(guid, wrapper): (&Guid, &Subcontainer)| {
|
|
||||||
format!(
|
|
||||||
"{guid} imageId: {image_id} name: \"{name}\"",
|
|
||||||
name = &wrapper.name,
|
|
||||||
image_id = &wrapper.image_id
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let Some((subcontainer_id, image_id)) = subcontainer_ids
|
let Some((subcontainer_id, image_id)) = subcontainer_ids
|
||||||
.first()
|
.first()
|
||||||
.map::<(Guid, ImageId), _>(|&x| (x.0.clone(), x.1.image_id.clone()))
|
.map::<(Guid, ImageId), _>(|&x| (x.0.clone(), x.1.image_id.clone()))
|
||||||
@@ -803,19 +810,17 @@ pub async fn attach(
|
|||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.iter()
|
.iter()
|
||||||
.map(format_subcontainer_pair)
|
.map(|(g, s)| SubcontainerInfo {
|
||||||
.join("\n");
|
id: g.clone(),
|
||||||
|
name: s.name.clone(),
|
||||||
|
image_id: s.image_id.clone(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!(
|
eyre!("{}", t!("service.mod.no-matching-subcontainers", id = id)),
|
||||||
"{}",
|
|
||||||
t!(
|
|
||||||
"service.mod.no-matching-subcontainers",
|
|
||||||
id = id,
|
|
||||||
subcontainers = subcontainers
|
|
||||||
)
|
|
||||||
),
|
|
||||||
ErrorKind::NotFound,
|
ErrorKind::NotFound,
|
||||||
));
|
)
|
||||||
|
.with_info(to_value(&subcontainers)?));
|
||||||
};
|
};
|
||||||
|
|
||||||
let passwd = root_dir
|
let passwd = root_dir
|
||||||
@@ -835,38 +840,39 @@ pub async fn attach(
|
|||||||
)
|
)
|
||||||
.with_kind(ErrorKind::Deserialization)?;
|
.with_kind(ErrorKind::Deserialization)?;
|
||||||
|
|
||||||
let root_command = get_passwd_command(
|
let user = user
|
||||||
passwd,
|
.clone()
|
||||||
user.as_deref()
|
.or_else(|| image_meta["user"].as_str().map(InternedString::intern))
|
||||||
.or_else(|| image_meta["user"].as_str())
|
.unwrap_or_else(|| InternedString::intern("root"));
|
||||||
.unwrap_or("root"),
|
|
||||||
)
|
let root_command = get_passwd_command(passwd, &*user).await;
|
||||||
.await;
|
|
||||||
|
|
||||||
let workdir = image_meta["workdir"].as_str().map(|s| s.to_owned());
|
let workdir = image_meta["workdir"].as_str().map(|s| s.to_owned());
|
||||||
|
|
||||||
if subcontainer_ids.len() > 1 {
|
if subcontainer_ids.len() > 1 {
|
||||||
let subcontainer_ids = subcontainer_ids
|
let subcontainers = subcontainer_ids
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(format_subcontainer_pair)
|
.map(|(g, s)| SubcontainerInfo {
|
||||||
.join("\n");
|
id: g.clone(),
|
||||||
|
name: s.name.clone(),
|
||||||
|
image_id: s.image_id.clone(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!(
|
eyre!(
|
||||||
"{}",
|
"{}",
|
||||||
t!(
|
t!("service.mod.multiple-subcontainers-found", id = id,)
|
||||||
"service.mod.multiple-subcontainers-found",
|
|
||||||
id = id,
|
|
||||||
subcontainer_ids = subcontainer_ids
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
ErrorKind::InvalidRequest,
|
ErrorKind::InvalidRequest,
|
||||||
));
|
)
|
||||||
|
.with_info(to_value(&subcontainers)?));
|
||||||
}
|
}
|
||||||
|
|
||||||
(
|
(
|
||||||
service_ref.container_id()?,
|
service_ref.container_id()?,
|
||||||
subcontainer_id,
|
subcontainer_id,
|
||||||
image_id,
|
image_id,
|
||||||
|
user.into(),
|
||||||
workdir,
|
workdir,
|
||||||
root_command,
|
root_command,
|
||||||
)
|
)
|
||||||
@@ -883,7 +889,7 @@ pub async fn attach(
|
|||||||
pty_size: Option<TermSize>,
|
pty_size: Option<TermSize>,
|
||||||
image_id: ImageId,
|
image_id: ImageId,
|
||||||
workdir: Option<String>,
|
workdir: Option<String>,
|
||||||
user: Option<InternedString>,
|
user: InternedString,
|
||||||
root_command: &RootCommand,
|
root_command: &RootCommand,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
use axum::extract::ws::Message;
|
use axum::extract::ws::Message;
|
||||||
@@ -904,11 +910,9 @@ pub async fn attach(
|
|||||||
Path::new("/media/startos/images")
|
Path::new("/media/startos/images")
|
||||||
.join(image_id)
|
.join(image_id)
|
||||||
.with_extension("env"),
|
.with_extension("env"),
|
||||||
);
|
)
|
||||||
|
.arg("--user")
|
||||||
if let Some(user) = user {
|
.arg(&*user);
|
||||||
cmd.arg("--user").arg(&*user);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(workdir) = workdir {
|
if let Some(workdir) = workdir {
|
||||||
cmd.arg("--workdir").arg(workdir);
|
cmd.arg("--workdir").arg(workdir);
|
||||||
@@ -1091,45 +1095,6 @@ pub async fn attach(
|
|||||||
Ok(guid)
|
Ok(guid)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, TS)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct ListSubcontainersParams {
|
|
||||||
pub id: PackageId,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct SubcontainerInfo {
|
|
||||||
pub name: InternedString,
|
|
||||||
pub image_id: ImageId,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_subcontainers(
|
|
||||||
ctx: RpcContext,
|
|
||||||
ListSubcontainersParams { id }: ListSubcontainersParams,
|
|
||||||
) -> Result<BTreeMap<Guid, SubcontainerInfo>, Error> {
|
|
||||||
let service = ctx.services.get(&id).await;
|
|
||||||
let service_ref = service.as_ref().or_not_found(&id)?;
|
|
||||||
let container = &service_ref.seed.persistent_container;
|
|
||||||
|
|
||||||
let subcontainers = container.subcontainers.lock().await;
|
|
||||||
|
|
||||||
let result: BTreeMap<Guid, SubcontainerInfo> = subcontainers
|
|
||||||
.iter()
|
|
||||||
.map(|(guid, subcontainer)| {
|
|
||||||
(
|
|
||||||
guid.clone(),
|
|
||||||
SubcontainerInfo {
|
|
||||||
name: subcontainer.name.clone(),
|
|
||||||
image_id: subcontainer.image_id.clone(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_passwd_command(etc_passwd_path: PathBuf, user: &str) -> RootCommand {
|
async fn get_passwd_command(etc_passwd_path: PathBuf, user: &str) -> RootCommand {
|
||||||
async {
|
async {
|
||||||
let mut file = tokio::fs::File::open(etc_passwd_path).await?;
|
let mut file = tokio::fs::File::open(etc_passwd_path).await?;
|
||||||
@@ -1210,23 +1175,34 @@ pub async fn cli_attach(
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let method = parent_method.into_iter().chain(method).join(".");
|
||||||
|
let mut params = json!({
|
||||||
|
"id": params.id,
|
||||||
|
"command": params.command,
|
||||||
|
"tty": tty,
|
||||||
|
"stderrTty": stderr.is_terminal(),
|
||||||
|
"ptySize": if tty { TermSize::get_current() } else { None },
|
||||||
|
"subcontainer": params.subcontainer,
|
||||||
|
"imageId": params.image_id,
|
||||||
|
"name": params.name,
|
||||||
|
"user": params.user,
|
||||||
|
});
|
||||||
let guid: Guid = from_value(
|
let guid: Guid = from_value(
|
||||||
context
|
match context
|
||||||
.call_remote::<RpcContext>(
|
.call_remote::<RpcContext>(&method, params.clone())
|
||||||
&parent_method.into_iter().chain(method).join("."),
|
.await
|
||||||
json!({
|
{
|
||||||
"id": params.id,
|
Ok(a) => a,
|
||||||
"command": params.command,
|
Err(e) => {
|
||||||
"tty": tty,
|
let prompt = e.to_string();
|
||||||
"stderrTty": stderr.is_terminal(),
|
let options: Vec<SubcontainerInfo> = from_value(e.info)?;
|
||||||
"ptySize": if tty { TermSize::get_current() } else { None },
|
let choice = choose(&prompt, &options).await?;
|
||||||
"subcontainer": params.subcontainer,
|
params["subcontainer"] = to_value(&choice.id)?;
|
||||||
"imageId": params.image_id,
|
context
|
||||||
"name": params.name,
|
.call_remote::<RpcContext>(&method, params.clone())
|
||||||
"user": params.user,
|
.await?
|
||||||
}),
|
}
|
||||||
)
|
},
|
||||||
.await?,
|
|
||||||
)?;
|
)?;
|
||||||
let mut ws = context.ws_continuation(guid).await?;
|
let mut ws = context.ws_continuation(guid).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use clap::{ArgAction, Parser};
|
|||||||
use color_eyre::eyre::{Result, eyre};
|
use color_eyre::eyre::{Result, eyre};
|
||||||
use exver::{Version, VersionRange};
|
use exver::{Version, VersionRange};
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
|
use imbl::OrdMap;
|
||||||
use imbl_value::json;
|
use imbl_value::json;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use patch_db::json_ptr::JsonPointer;
|
use patch_db::json_ptr::JsonPointer;
|
||||||
@@ -245,6 +246,7 @@ async fn maybe_do_update(
|
|||||||
let mut available = from_value::<BTreeMap<Version, OsVersionInfo>>(
|
let mut available = from_value::<BTreeMap<Version, OsVersionInfo>>(
|
||||||
ctx.call_remote_with::<RegistryContext, _>(
|
ctx.call_remote_with::<RegistryContext, _>(
|
||||||
"os.version.get",
|
"os.version.get",
|
||||||
|
OrdMap::new(),
|
||||||
json!({
|
json!({
|
||||||
"source": current_version,
|
"source": current_version,
|
||||||
"target": target,
|
"target": target,
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ impl<'a> Invoke<'a> for ExtendedCommand<'a> {
|
|||||||
.or(Some(&res.stdout))
|
.or(Some(&res.stdout))
|
||||||
.filter(|a| !a.is_empty())
|
.filter(|a| !a.is_empty())
|
||||||
.and_then(|a| std::str::from_utf8(a).ok())
|
.and_then(|a| std::str::from_utf8(a).ok())
|
||||||
.unwrap_or(&format!("{} exited with code {}", cmd_str, res.status))
|
.unwrap_or(&format!("{} exited with {}", cmd_str, res.status))
|
||||||
);
|
);
|
||||||
Ok(res.stdout)
|
Ok(res.stdout)
|
||||||
} else {
|
} else {
|
||||||
@@ -309,7 +309,7 @@ impl<'a> Invoke<'a> for ExtendedCommand<'a> {
|
|||||||
.filter(|a| !a.is_empty())
|
.filter(|a| !a.is_empty())
|
||||||
.and_then(|a| std::str::from_utf8(a).ok())
|
.and_then(|a| std::str::from_utf8(a).ok())
|
||||||
.unwrap_or(&format!(
|
.unwrap_or(&format!(
|
||||||
"{} exited with code {}",
|
"{} exited with {}",
|
||||||
cmd.as_std().get_program().to_string_lossy(),
|
cmd.as_std().get_program().to_string_lossy(),
|
||||||
res.status
|
res.status
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -58,8 +58,9 @@ mod v0_4_0_alpha_15;
|
|||||||
mod v0_4_0_alpha_16;
|
mod v0_4_0_alpha_16;
|
||||||
mod v0_4_0_alpha_17;
|
mod v0_4_0_alpha_17;
|
||||||
mod v0_4_0_alpha_18;
|
mod v0_4_0_alpha_18;
|
||||||
|
mod v0_4_0_alpha_19;
|
||||||
|
|
||||||
pub type Current = v0_4_0_alpha_18::Version; // VERSION_BUMP
|
pub type Current = v0_4_0_alpha_19::Version; // VERSION_BUMP
|
||||||
|
|
||||||
impl Current {
|
impl Current {
|
||||||
#[instrument(skip(self, db))]
|
#[instrument(skip(self, db))]
|
||||||
@@ -179,7 +180,8 @@ enum Version {
|
|||||||
V0_4_0_alpha_15(Wrapper<v0_4_0_alpha_15::Version>),
|
V0_4_0_alpha_15(Wrapper<v0_4_0_alpha_15::Version>),
|
||||||
V0_4_0_alpha_16(Wrapper<v0_4_0_alpha_16::Version>),
|
V0_4_0_alpha_16(Wrapper<v0_4_0_alpha_16::Version>),
|
||||||
V0_4_0_alpha_17(Wrapper<v0_4_0_alpha_17::Version>),
|
V0_4_0_alpha_17(Wrapper<v0_4_0_alpha_17::Version>),
|
||||||
V0_4_0_alpha_18(Wrapper<v0_4_0_alpha_18::Version>), // VERSION_BUMP
|
V0_4_0_alpha_18(Wrapper<v0_4_0_alpha_18::Version>),
|
||||||
|
V0_4_0_alpha_19(Wrapper<v0_4_0_alpha_19::Version>), // VERSION_BUMP
|
||||||
Other(exver::Version),
|
Other(exver::Version),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +242,8 @@ impl Version {
|
|||||||
Self::V0_4_0_alpha_15(v) => DynVersion(Box::new(v.0)),
|
Self::V0_4_0_alpha_15(v) => DynVersion(Box::new(v.0)),
|
||||||
Self::V0_4_0_alpha_16(v) => DynVersion(Box::new(v.0)),
|
Self::V0_4_0_alpha_16(v) => DynVersion(Box::new(v.0)),
|
||||||
Self::V0_4_0_alpha_17(v) => DynVersion(Box::new(v.0)),
|
Self::V0_4_0_alpha_17(v) => DynVersion(Box::new(v.0)),
|
||||||
Self::V0_4_0_alpha_18(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
|
Self::V0_4_0_alpha_18(v) => DynVersion(Box::new(v.0)),
|
||||||
|
Self::V0_4_0_alpha_19(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
|
||||||
Self::Other(v) => {
|
Self::Other(v) => {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!("unknown version {v}"),
|
eyre!("unknown version {v}"),
|
||||||
@@ -293,7 +296,8 @@ impl Version {
|
|||||||
Version::V0_4_0_alpha_15(Wrapper(x)) => x.semver(),
|
Version::V0_4_0_alpha_15(Wrapper(x)) => x.semver(),
|
||||||
Version::V0_4_0_alpha_16(Wrapper(x)) => x.semver(),
|
Version::V0_4_0_alpha_16(Wrapper(x)) => x.semver(),
|
||||||
Version::V0_4_0_alpha_17(Wrapper(x)) => x.semver(),
|
Version::V0_4_0_alpha_17(Wrapper(x)) => x.semver(),
|
||||||
Version::V0_4_0_alpha_18(Wrapper(x)) => x.semver(), // VERSION_BUMP
|
Version::V0_4_0_alpha_18(Wrapper(x)) => x.semver(),
|
||||||
|
Version::V0_4_0_alpha_19(Wrapper(x)) => x.semver(), // VERSION_BUMP
|
||||||
Version::Other(x) => x.clone(),
|
Version::Other(x) => x.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
core/src/version/v0_4_0_alpha_19.rs
Normal file
37
core/src/version/v0_4_0_alpha_19.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
use exver::{PreReleaseSegment, VersionRange};
|
||||||
|
|
||||||
|
use super::v0_3_5::V0_3_0_COMPAT;
|
||||||
|
use super::{VersionT, v0_4_0_alpha_18};
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref V0_4_0_alpha_19: exver::Version = exver::Version::new(
|
||||||
|
[0, 4, 0],
|
||||||
|
[PreReleaseSegment::String("alpha".into()), 19.into()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct Version;
|
||||||
|
|
||||||
|
impl VersionT for Version {
|
||||||
|
type Previous = v0_4_0_alpha_18::Version;
|
||||||
|
type PreUpRes = ();
|
||||||
|
|
||||||
|
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn semver(self) -> exver::Version {
|
||||||
|
V0_4_0_alpha_19.clone()
|
||||||
|
}
|
||||||
|
fn compat(self) -> &'static VersionRange {
|
||||||
|
&V0_3_0_COMPAT
|
||||||
|
}
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
||||||
|
Ok(Value::Null)
|
||||||
|
}
|
||||||
|
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -132,7 +132,6 @@ export type SDKManifest = {
|
|||||||
* `pattern` refers to a regular expression that at least one device of the specified class must match
|
* `pattern` refers to a regular expression that at least one device of the specified class must match
|
||||||
* `patternDescription` is what will be displayed to the user about what kind of device is required
|
* `patternDescription` is what will be displayed to the user about what kind of device is required
|
||||||
* @property {number} ram - Minimum RAM requirement (in megabytes MB)
|
* @property {number} ram - Minimum RAM requirement (in megabytes MB)
|
||||||
* @property {string[]} arch - List of supported arches
|
|
||||||
* @example
|
* @example
|
||||||
* ```
|
* ```
|
||||||
hardwareRequirements: {
|
hardwareRequirements: {
|
||||||
@@ -141,14 +140,12 @@ export type SDKManifest = {
|
|||||||
{ class: 'processor', pattern: 'i[3579]-10[0-9]{3}U CPU', patternDescription: 'A 10th Generation Intel i-Series processor' },
|
{ class: 'processor', pattern: 'i[3579]-10[0-9]{3}U CPU', patternDescription: 'A 10th Generation Intel i-Series processor' },
|
||||||
],
|
],
|
||||||
ram: 8192,
|
ram: 8192,
|
||||||
arch: ['x86-64'],
|
|
||||||
},
|
},
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
readonly hardwareRequirements?: {
|
readonly hardwareRequirements?: {
|
||||||
readonly device?: T.DeviceFilter[]
|
readonly device?: T.DeviceFilter[]
|
||||||
readonly ram?: number | null
|
readonly ram?: number | null
|
||||||
readonly arch?: string[] | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ import {
|
|||||||
import { getOwnServiceInterfaces } from "../../base/lib/util/getServiceInterfaces"
|
import { getOwnServiceInterfaces } from "../../base/lib/util/getServiceInterfaces"
|
||||||
import { Volumes, createVolumes } from "./util/Volume"
|
import { Volumes, createVolumes } from "./util/Volume"
|
||||||
|
|
||||||
export const OSVersion = testTypeVersion("0.4.0-alpha.18")
|
export const OSVersion = testTypeVersion("0.4.0-alpha.19")
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
type AnyNeverCond<T extends any[], Then, Else> =
|
type AnyNeverCond<T extends any[], Then, Else> =
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ export function buildManifest<
|
|||||||
): Manifest & T.Manifest {
|
): Manifest & T.Manifest {
|
||||||
const images = Object.entries(manifest.images).reduce(
|
const images = Object.entries(manifest.images).reduce(
|
||||||
(images, [k, v]) => {
|
(images, [k, v]) => {
|
||||||
v.arch = v.arch || ["aarch64", "x86_64"]
|
v.arch = v.arch ?? ["aarch64", "x86_64", "riscv64"]
|
||||||
if (v.emulateMissingAs === undefined)
|
if (v.emulateMissingAs === undefined)
|
||||||
v.emulateMissingAs = (v.arch as string[]).includes("aarch64")
|
v.emulateMissingAs = (v.arch as string[]).includes("x86_64")
|
||||||
? "aarch64"
|
? "x86_64"
|
||||||
: v.arch[0] || null
|
: (v.arch[0] ?? null)
|
||||||
v.nvidiaContainer = !!v.nvidiaContainer
|
v.nvidiaContainer = !!v.nvidiaContainer
|
||||||
images[k] = v as ImageConfig
|
images[k] = v as ImageConfig
|
||||||
return images
|
return images
|
||||||
@@ -75,21 +75,18 @@ export function buildManifest<
|
|||||||
hardwareRequirements: {
|
hardwareRequirements: {
|
||||||
device: manifest.hardwareRequirements?.device || [],
|
device: manifest.hardwareRequirements?.device || [],
|
||||||
ram: manifest.hardwareRequirements?.ram || null,
|
ram: manifest.hardwareRequirements?.ram || null,
|
||||||
arch:
|
arch: Object.values(images).reduce(
|
||||||
manifest.hardwareRequirements?.arch === undefined
|
(arch, inputSpec) => {
|
||||||
? Object.values(images).reduce(
|
if (inputSpec.emulateMissingAs) {
|
||||||
(arch, inputSpec) => {
|
return arch
|
||||||
if (inputSpec.emulateMissingAs) {
|
}
|
||||||
return arch
|
if (arch === null) {
|
||||||
}
|
return inputSpec.arch
|
||||||
if (arch === null) {
|
}
|
||||||
return inputSpec.arch
|
return arch.filter((a) => inputSpec.arch.includes(a))
|
||||||
}
|
},
|
||||||
return arch.filter((a) => inputSpec.arch.includes(a))
|
null as string[] | null,
|
||||||
},
|
),
|
||||||
null as string[] | null,
|
|
||||||
)
|
|
||||||
: manifest.hardwareRequirements?.arch,
|
|
||||||
},
|
},
|
||||||
hardwareAcceleration: manifest.hardwareAcceleration ?? false,
|
hardwareAcceleration: manifest.hardwareAcceleration ?? false,
|
||||||
}
|
}
|
||||||
|
|||||||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-alpha.18",
|
"version": "0.4.0-alpha.19",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-alpha.18",
|
"version": "0.4.0-alpha.19",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^20.3.0",
|
"@angular/animations": "^20.3.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-alpha.18",
|
"version": "0.4.0-alpha.19",
|
||||||
"author": "Start9 Labs, Inc",
|
"author": "Start9 Labs, Inc",
|
||||||
"homepage": "https://start9.com/",
|
"homepage": "https://start9.com/",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
{{ pkg.title }}
|
{{ pkg.title }}
|
||||||
</span>
|
</span>
|
||||||
<span class="detail-description">
|
<span class="detail-description">
|
||||||
{{ pkg.description.short }}
|
{{ pkg.description.short | localize }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
import { SharedPipesModule, TickerComponent } from '@start9labs/shared'
|
import { LocalizePipe, SharedPipesModule, TickerComponent } from '@start9labs/shared'
|
||||||
import { ItemComponent } from './item.component'
|
import { ItemComponent } from './item.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [ItemComponent],
|
declarations: [ItemComponent],
|
||||||
exports: [ItemComponent],
|
exports: [ItemComponent],
|
||||||
imports: [CommonModule, RouterModule, SharedPipesModule, TickerComponent],
|
imports: [CommonModule, RouterModule, SharedPipesModule, TickerComponent, LocalizePipe],
|
||||||
})
|
})
|
||||||
export class ItemModule {}
|
export class ItemModule {}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
output,
|
output,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { MarketplacePkgBase } from '../../types'
|
import { MarketplacePkgBase } from '../../types'
|
||||||
import { CopyService, i18nPipe } from '@start9labs/shared'
|
import { CopyService, i18nPipe, LocalizePipe } from '@start9labs/shared'
|
||||||
import { DatePipe } from '@angular/common'
|
import { DatePipe } from '@angular/common'
|
||||||
import { MarketplaceItemComponent } from './item.component'
|
import { MarketplaceItemComponent } from './item.component'
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ import { MarketplaceItemComponent } from './item.component'
|
|||||||
<div class="background-border box-shadow-lg shadow-color-light">
|
<div class="background-border box-shadow-lg shadow-color-light">
|
||||||
<div class="box-container">
|
<div class="box-container">
|
||||||
<h2 class="additional-detail-title">{{ 'Description' | i18n }}</h2>
|
<h2 class="additional-detail-title">{{ 'Description' | i18n }}</h2>
|
||||||
<p [innerHTML]="pkg().description.long"></p>
|
<p [innerHTML]="pkg().description.long | localize"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
@@ -129,7 +129,7 @@ import { MarketplaceItemComponent } from './item.component'
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [MarketplaceItemComponent, DatePipe, i18nPipe],
|
imports: [MarketplaceItemComponent, DatePipe, i18nPipe, LocalizePipe],
|
||||||
})
|
})
|
||||||
export class MarketplaceAboutComponent {
|
export class MarketplaceAboutComponent {
|
||||||
readonly copyService = inject(CopyService)
|
readonly copyService = inject(CopyService)
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { i18nPipe } from '@start9labs/shared'
|
||||||
|
import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
|
||||||
|
import { injectContext } from '@taiga-ui/polymorpheus'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
imports: [TuiButton, i18nPipe],
|
||||||
|
template: `
|
||||||
|
<div class="animation-container">
|
||||||
|
<div class="port">
|
||||||
|
<div class="port-inner"></div>
|
||||||
|
</div>
|
||||||
|
<div class="usb-stick">
|
||||||
|
<div class="usb-connector"></div>
|
||||||
|
<div class="usb-body"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
'Remove USB stick or other installation media from your server' | i18n
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<footer>
|
||||||
|
<button tuiButton (click)="context.completeWith(true)">
|
||||||
|
{{ 'Done' | i18n }}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
`,
|
||||||
|
styles: `
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-container {
|
||||||
|
position: relative;
|
||||||
|
width: 160px;
|
||||||
|
height: 69px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.port {
|
||||||
|
position: absolute;
|
||||||
|
left: 20px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 28px;
|
||||||
|
height: 18px;
|
||||||
|
background: var(--tui-background-neutral-1);
|
||||||
|
border: 2px solid var(--tui-border-normal);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.port-inner {
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
right: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
background: var(--tui-background-neutral-2);
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usb-stick {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
animation: slide-out 2s ease-in-out 0.5s infinite;
|
||||||
|
left: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usb-connector {
|
||||||
|
width: 20px;
|
||||||
|
height: 12px;
|
||||||
|
background: var(--tui-text-secondary);
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usb-body {
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
background: var(--tui-status-info);
|
||||||
|
border-radius: 2px 4px 4px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-out {
|
||||||
|
0% {
|
||||||
|
left: 32px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
5% {
|
||||||
|
left: 32px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
left: 130px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 130px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class RemoveMediaDialog {
|
||||||
|
protected readonly context = injectContext<TuiDialogContext<boolean>>()
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { ChangeDetectorRef, Component, inject } from '@angular/core'
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
HostListener,
|
||||||
|
inject,
|
||||||
|
} from '@angular/core'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import {
|
import {
|
||||||
@@ -21,13 +26,14 @@ import {
|
|||||||
import { TuiDataListWrapper, TuiSelect, TuiTooltip } from '@taiga-ui/kit'
|
import { TuiDataListWrapper, TuiSelect, TuiTooltip } from '@taiga-ui/kit'
|
||||||
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
|
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
|
||||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||||
import { filter } from 'rxjs'
|
import { filter, Subscription } from 'rxjs'
|
||||||
import { ApiService } from '../services/api.service'
|
import { ApiService } from '../services/api.service'
|
||||||
import { StateService } from '../services/state.service'
|
import { StateService } from '../services/state.service'
|
||||||
import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog'
|
import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
|
@if (!shuttingDown) {
|
||||||
<section tuiCardLarge="compact">
|
<section tuiCardLarge="compact">
|
||||||
<header tuiHeader>
|
<header tuiHeader>
|
||||||
<h2 tuiTitle>{{ 'Select Drives' | i18n }}</h2>
|
<h2 tuiTitle>{{ 'Select Drives' | i18n }}</h2>
|
||||||
@@ -132,6 +138,7 @@ import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog
|
|||||||
}
|
}
|
||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
.no-drives {
|
.no-drives {
|
||||||
@@ -176,6 +183,14 @@ export default class DrivesPage {
|
|||||||
|
|
||||||
protected readonly mobile = inject(TUI_IS_MOBILE)
|
protected readonly mobile = inject(TUI_IS_MOBILE)
|
||||||
|
|
||||||
|
@HostListener('document:keydown', ['$event'])
|
||||||
|
onKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.ctrlKey && event.shiftKey && event.key === 'X') {
|
||||||
|
event.preventDefault()
|
||||||
|
this.shutdownServer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
readonly osDriveTooltip = this.i18n.transform(
|
readonly osDriveTooltip = this.i18n.transform(
|
||||||
'The drive where the StartOS operating system will be installed.',
|
'The drive where the StartOS operating system will be installed.',
|
||||||
)
|
)
|
||||||
@@ -185,6 +200,8 @@ export default class DrivesPage {
|
|||||||
|
|
||||||
drives: DiskInfo[] = []
|
drives: DiskInfo[] = []
|
||||||
loading = true
|
loading = true
|
||||||
|
shuttingDown = false
|
||||||
|
private dialogSub?: Subscription
|
||||||
selectedOsDrive: DiskInfo | null = null
|
selectedOsDrive: DiskInfo | null = null
|
||||||
selectedDataDrive: DiskInfo | null = null
|
selectedDataDrive: DiskInfo | null = null
|
||||||
preserveData: boolean | null = null
|
preserveData: boolean | null = null
|
||||||
@@ -339,22 +356,18 @@ export default class DrivesPage {
|
|||||||
loader.unsubscribe()
|
loader.unsubscribe()
|
||||||
|
|
||||||
// Show success dialog
|
// Show success dialog
|
||||||
this.dialogs
|
this.dialogSub = this.dialogs
|
||||||
.openConfirm({
|
.openAlert('StartOS has been installed successfully.', {
|
||||||
label: 'Installation Complete!',
|
label: 'Installation Complete!',
|
||||||
size: 's',
|
size: 's',
|
||||||
data: {
|
dismissible: false,
|
||||||
content: 'StartOS has been installed successfully.',
|
closeable: true,
|
||||||
yes: 'Continue to Setup',
|
data: { button: this.i18n.transform('Continue to Setup') },
|
||||||
no: 'Shutdown',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.subscribe(continueSetup => {
|
.subscribe({
|
||||||
if (continueSetup) {
|
complete: () => {
|
||||||
this.navigateToNextStep(result.attach)
|
this.navigateToNextStep(result.attach)
|
||||||
} else {
|
},
|
||||||
this.shutdownServer()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
loader.unsubscribe()
|
loader.unsubscribe()
|
||||||
@@ -372,10 +385,12 @@ export default class DrivesPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async shutdownServer() {
|
private async shutdownServer() {
|
||||||
|
this.dialogSub?.unsubscribe()
|
||||||
const loader = this.loader.open('Beginning shutdown').subscribe()
|
const loader = this.loader.open('Beginning shutdown').subscribe()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.shutdown()
|
await this.api.shutdown()
|
||||||
|
this.shuttingDown = true
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
DOCUMENT,
|
DOCUMENT,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { DownloadHTMLService, ErrorService, i18nPipe } from '@start9labs/shared'
|
import {
|
||||||
|
DialogService,
|
||||||
|
DownloadHTMLService,
|
||||||
|
ErrorService,
|
||||||
|
i18nPipe,
|
||||||
|
} from '@start9labs/shared'
|
||||||
import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core'
|
import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core'
|
||||||
import { TuiAvatar } from '@taiga-ui/kit'
|
import { TuiAvatar } from '@taiga-ui/kit'
|
||||||
import { TuiCardLarge, TuiCell, TuiHeader } from '@taiga-ui/layout'
|
import { TuiCardLarge, TuiCell, TuiHeader } from '@taiga-ui/layout'
|
||||||
@@ -14,7 +19,9 @@ import { ApiService } from '../services/api.service'
|
|||||||
import { StateService } from '../services/state.service'
|
import { StateService } from '../services/state.service'
|
||||||
import { DocumentationComponent } from '../components/documentation.component'
|
import { DocumentationComponent } from '../components/documentation.component'
|
||||||
import { MatrixComponent } from '../components/matrix.component'
|
import { MatrixComponent } from '../components/matrix.component'
|
||||||
|
import { RemoveMediaDialog } from '../components/remove-media.dialog'
|
||||||
import { SetupCompleteRes } from '../types'
|
import { SetupCompleteRes } from '../types'
|
||||||
|
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@@ -29,12 +36,8 @@ import { SetupCompleteRes } from '../types'
|
|||||||
@if (!stateService.kiosk) {
|
@if (!stateService.kiosk) {
|
||||||
<span tuiSubtitle>
|
<span tuiSubtitle>
|
||||||
{{
|
{{
|
||||||
stateService.setupType === 'restore'
|
'http://start.local was for setup only. It will no longer work.'
|
||||||
? ('You can unplug your backup drive' | i18n)
|
| i18n
|
||||||
: stateService.setupType === 'transfer'
|
|
||||||
? ('You can unplug your transfer drive' | i18n)
|
|
||||||
: ('http://start.local was for setup only. It will no longer work.'
|
|
||||||
| i18n)
|
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -69,14 +72,15 @@ import { SetupCompleteRes } from '../types'
|
|||||||
tuiCell="l"
|
tuiCell="l"
|
||||||
[class.disabled]="!stateService.kiosk && !downloaded"
|
[class.disabled]="!stateService.kiosk && !downloaded"
|
||||||
[disabled]="(!stateService.kiosk && !downloaded) || usbRemoved"
|
[disabled]="(!stateService.kiosk && !downloaded) || usbRemoved"
|
||||||
(click)="usbRemoved = true"
|
(click)="removeMedia()"
|
||||||
>
|
>
|
||||||
<tui-avatar appearance="secondary" src="@tui.usb" />
|
<tui-avatar appearance="secondary" src="@tui.usb" />
|
||||||
<div tuiTitle>
|
<div tuiTitle>
|
||||||
{{ 'USB Removed' | i18n }}
|
{{ 'Remove Installation Media' | i18n }}
|
||||||
<div tuiSubtitle>
|
<div tuiSubtitle>
|
||||||
{{
|
{{
|
||||||
'Remove the USB installation media from your server' | i18n
|
'Remove USB stick or other installation media from your server'
|
||||||
|
| i18n
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,6 +188,7 @@ export default class SuccessPage implements AfterViewInit {
|
|||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly downloadHtml = inject(DownloadHTMLService)
|
private readonly downloadHtml = inject(DownloadHTMLService)
|
||||||
|
private readonly dialogs = inject(DialogService)
|
||||||
private readonly i18n = inject(i18nPipe)
|
private readonly i18n = inject(i18nPipe)
|
||||||
|
|
||||||
readonly stateService = inject(StateService)
|
readonly stateService = inject(StateService)
|
||||||
@@ -225,6 +230,21 @@ export default class SuccessPage implements AfterViewInit {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeMedia() {
|
||||||
|
this.dialogs
|
||||||
|
.openComponent<boolean>(
|
||||||
|
new PolymorpheusComponent(RemoveMediaDialog),
|
||||||
|
{
|
||||||
|
size: 's',
|
||||||
|
dismissible: false,
|
||||||
|
closeable: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.usbRemoved = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
exitKiosk() {
|
exitKiosk() {
|
||||||
this.api.exit()
|
this.api.exit()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export default {
|
|||||||
102: 'Verlassen',
|
102: 'Verlassen',
|
||||||
103: 'Sind Sie sicher?',
|
103: 'Sind Sie sicher?',
|
||||||
104: 'Neues Netzwerk-Gateway',
|
104: 'Neues Netzwerk-Gateway',
|
||||||
|
107: 'Onion-Domains',
|
||||||
108: 'Öffentlich',
|
108: 'Öffentlich',
|
||||||
109: 'privat',
|
109: 'privat',
|
||||||
111: 'Keine Onion-Domains',
|
111: 'Keine Onion-Domains',
|
||||||
@@ -639,13 +640,11 @@ export default {
|
|||||||
667: 'Einrichtung wird gestartet',
|
667: 'Einrichtung wird gestartet',
|
||||||
670: 'Warten Sie 1–2 Minuten und aktualisieren Sie die Seite',
|
670: 'Warten Sie 1–2 Minuten und aktualisieren Sie die Seite',
|
||||||
672: 'Einrichtung abgeschlossen!',
|
672: 'Einrichtung abgeschlossen!',
|
||||||
673: 'Sie können Ihr Backup-Laufwerk entfernen',
|
|
||||||
674: 'Sie können Ihr Übertragungs-Laufwerk entfernen',
|
|
||||||
675: 'http://start.local war nur für die Einrichtung gedacht. Es funktioniert nicht mehr.',
|
675: 'http://start.local war nur für die Einrichtung gedacht. Es funktioniert nicht mehr.',
|
||||||
676: 'Adressinformationen herunterladen',
|
676: 'Adressinformationen herunterladen',
|
||||||
677: 'Enthält die permanente lokale Adresse Ihres Servers und die Root-CA',
|
677: 'Enthält die permanente lokale Adresse Ihres Servers und die Root-CA',
|
||||||
678: 'USB entfernt',
|
678: 'Installationsmedium entfernen',
|
||||||
679: 'Entfernen Sie das USB-Installationsmedium aus Ihrem Server',
|
679: 'Entfernen Sie den USB-Stick oder andere Installationsmedien von Ihrem Server',
|
||||||
680: 'Server neu starten',
|
680: 'Server neu starten',
|
||||||
681: 'Warten, bis der Server wieder online ist',
|
681: 'Warten, bis der Server wieder online ist',
|
||||||
682: 'Server ist wieder online',
|
682: 'Server ist wieder online',
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'Leave': 102,
|
'Leave': 102,
|
||||||
'Are you sure?': 103,
|
'Are you sure?': 103,
|
||||||
'New gateway': 104, // as in, a network gateway
|
'New gateway': 104, // as in, a network gateway
|
||||||
|
'Tor Domains': 107,
|
||||||
'public': 108,
|
'public': 108,
|
||||||
'private': 109,
|
'private': 109,
|
||||||
'No Tor domains': 111,
|
'No Tor domains': 111,
|
||||||
@@ -639,13 +640,11 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'Starting setup': 667,
|
'Starting setup': 667,
|
||||||
'Wait 1-2 minutes and refresh the page': 670,
|
'Wait 1-2 minutes and refresh the page': 670,
|
||||||
'Setup Complete!': 672,
|
'Setup Complete!': 672,
|
||||||
'You can unplug your backup drive': 673,
|
|
||||||
'You can unplug your transfer drive': 674,
|
|
||||||
'http://start.local was for setup only. It will no longer work.': 675,
|
'http://start.local was for setup only. It will no longer work.': 675,
|
||||||
'Download Address Info': 676,
|
'Download Address Info': 676,
|
||||||
"Contains your server's permanent local address and Root CA": 677,
|
"Contains your server's permanent local address and Root CA": 677,
|
||||||
'USB Removed': 678,
|
'Remove Installation Media': 678,
|
||||||
'Remove the USB installation media from your server': 679,
|
'Remove USB stick or other installation media from your server': 679,
|
||||||
'Restart Server': 680,
|
'Restart Server': 680,
|
||||||
'Waiting for server to come back online': 681,
|
'Waiting for server to come back online': 681,
|
||||||
'Server is back online': 682,
|
'Server is back online': 682,
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export default {
|
|||||||
102: 'Salir',
|
102: 'Salir',
|
||||||
103: '¿Estás seguro?',
|
103: '¿Estás seguro?',
|
||||||
104: 'Nueva puerta de enlace de red',
|
104: 'Nueva puerta de enlace de red',
|
||||||
|
107: 'dominios onion',
|
||||||
108: 'público',
|
108: 'público',
|
||||||
109: 'privado',
|
109: 'privado',
|
||||||
111: 'Sin dominios onion',
|
111: 'Sin dominios onion',
|
||||||
@@ -639,13 +640,11 @@ export default {
|
|||||||
667: 'Iniciando configuración',
|
667: 'Iniciando configuración',
|
||||||
670: 'Espere 1–2 minutos y actualice la página',
|
670: 'Espere 1–2 minutos y actualice la página',
|
||||||
672: '¡Configuración completa!',
|
672: '¡Configuración completa!',
|
||||||
673: 'Puede desconectar su unidad de copia de seguridad',
|
|
||||||
674: 'Puede desconectar su unidad de transferencia',
|
|
||||||
675: 'http://start.local era solo para la configuración. Ya no funcionará.',
|
675: 'http://start.local era solo para la configuración. Ya no funcionará.',
|
||||||
676: 'Descargar información de direcciones',
|
676: 'Descargar información de direcciones',
|
||||||
677: 'Contiene la dirección local permanente de su servidor y la CA raíz',
|
677: 'Contiene la dirección local permanente de su servidor y la CA raíz',
|
||||||
678: 'USB retirado',
|
678: 'Retirar medio de instalación',
|
||||||
679: 'Retire el medio de instalación USB de su servidor',
|
679: 'Retire la memoria USB u otro medio de instalación de su servidor',
|
||||||
680: 'Reiniciar servidor',
|
680: 'Reiniciar servidor',
|
||||||
681: 'Esperando a que el servidor vuelva a estar en línea',
|
681: 'Esperando a que el servidor vuelva a estar en línea',
|
||||||
682: 'El servidor ha vuelto a estar en línea',
|
682: 'El servidor ha vuelto a estar en línea',
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export default {
|
|||||||
102: 'Quitter',
|
102: 'Quitter',
|
||||||
103: 'Êtes-vous sûr ?',
|
103: 'Êtes-vous sûr ?',
|
||||||
104: 'Nouvelle passerelle réseau',
|
104: 'Nouvelle passerelle réseau',
|
||||||
|
107: 'domaine onion',
|
||||||
108: 'public',
|
108: 'public',
|
||||||
109: 'privé',
|
109: 'privé',
|
||||||
111: 'Aucune domaine onion',
|
111: 'Aucune domaine onion',
|
||||||
@@ -639,13 +640,11 @@ export default {
|
|||||||
667: 'Démarrage de la configuration',
|
667: 'Démarrage de la configuration',
|
||||||
670: 'Attendez 1 à 2 minutes puis actualisez la page',
|
670: 'Attendez 1 à 2 minutes puis actualisez la page',
|
||||||
672: 'Configuration terminée !',
|
672: 'Configuration terminée !',
|
||||||
673: 'Vous pouvez débrancher votre disque de sauvegarde',
|
|
||||||
674: 'Vous pouvez débrancher votre disque de transfert',
|
|
||||||
675: 'http://start.local était réservé à la configuration. Il ne fonctionnera plus.',
|
675: 'http://start.local était réservé à la configuration. Il ne fonctionnera plus.',
|
||||||
676: 'Télécharger les informations d’adresse',
|
676: 'Télécharger les informations d’adresse',
|
||||||
677: 'Contient l’adresse locale permanente de votre serveur et la CA racine',
|
677: 'Contient l\u2019adresse locale permanente de votre serveur et la CA racine',
|
||||||
678: 'USB retiré',
|
678: 'Retirer le support d\u2019installation',
|
||||||
679: 'Retirez le support d’installation USB de votre serveur',
|
679: 'Retirez la clé USB ou tout autre support d\u2019installation de votre serveur',
|
||||||
680: 'Redémarrer le serveur',
|
680: 'Redémarrer le serveur',
|
||||||
681: 'En attente du retour en ligne du serveur',
|
681: 'En attente du retour en ligne du serveur',
|
||||||
682: 'Le serveur est de nouveau en ligne',
|
682: 'Le serveur est de nouveau en ligne',
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export default {
|
|||||||
102: 'Opuść',
|
102: 'Opuść',
|
||||||
103: 'Czy jesteś pewien?',
|
103: 'Czy jesteś pewien?',
|
||||||
104: 'Nowa brama sieciowa',
|
104: 'Nowa brama sieciowa',
|
||||||
|
107: 'domeny onion',
|
||||||
108: 'publiczny',
|
108: 'publiczny',
|
||||||
109: 'prywatny',
|
109: 'prywatny',
|
||||||
111: 'Brak domeny onion',
|
111: 'Brak domeny onion',
|
||||||
@@ -639,13 +640,11 @@ export default {
|
|||||||
667: 'Rozpoczynanie konfiguracji',
|
667: 'Rozpoczynanie konfiguracji',
|
||||||
670: 'Poczekaj 1–2 minuty i odśwież stronę',
|
670: 'Poczekaj 1–2 minuty i odśwież stronę',
|
||||||
672: 'Konfiguracja zakończona!',
|
672: 'Konfiguracja zakończona!',
|
||||||
673: 'Możesz odłączyć dysk kopii zapasowej',
|
|
||||||
674: 'Możesz odłączyć dysk transferowy',
|
|
||||||
675: 'http://start.local służył tylko do konfiguracji. Nie będzie już działać.',
|
675: 'http://start.local służył tylko do konfiguracji. Nie będzie już działać.',
|
||||||
676: 'Pobierz informacje adresowe',
|
676: 'Pobierz informacje adresowe',
|
||||||
677: 'Zawiera stały lokalny adres serwera oraz główny urząd certyfikacji (Root CA)',
|
677: 'Zawiera stały lokalny adres serwera oraz główny urząd certyfikacji (Root CA)',
|
||||||
678: 'USB usunięty',
|
678: 'Usuń nośnik instalacyjny',
|
||||||
679: 'Usuń instalacyjny nośnik USB z serwera',
|
679: 'Usuń pamięć USB lub inny nośnik instalacyjny z serwera',
|
||||||
680: 'Uruchom ponownie serwer',
|
680: 'Uruchom ponownie serwer',
|
||||||
681: 'Oczekiwanie na ponowne połączenie serwera',
|
681: 'Oczekiwanie na ponowne połączenie serwera',
|
||||||
682: 'Serwer jest ponownie online',
|
682: 'Serwer jest ponownie online',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { inject, Injectable, Pipe, PipeTransform } from '@angular/core'
|
import { inject, Injectable, Pipe, PipeTransform } from '@angular/core'
|
||||||
import { i18nService } from './i18n.service'
|
import { i18nService } from './i18n.service'
|
||||||
|
import { I18N } from './i18n.providers'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
@@ -9,8 +10,10 @@ import { T } from '@start9labs/start-sdk'
|
|||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class LocalizePipe implements PipeTransform {
|
export class LocalizePipe implements PipeTransform {
|
||||||
private readonly i18nService = inject(i18nService)
|
private readonly i18nService = inject(i18nService)
|
||||||
|
private readonly i18n = inject(I18N)
|
||||||
|
|
||||||
transform(string: T.LocaleString): string {
|
transform(string: T.LocaleString): string {
|
||||||
|
this.i18n() // read signal to trigger change detection on language switch
|
||||||
return this.i18nService.localize(string)
|
return this.i18nService.localize(string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ type OnionForm = {
|
|||||||
selector: 'section[torDomains]',
|
selector: 'section[torDomains]',
|
||||||
template: `
|
template: `
|
||||||
<header>
|
<header>
|
||||||
Tor Domains
|
{{ 'Tor Domains' | i18n }}
|
||||||
<a
|
<a
|
||||||
tuiIconButton
|
tuiIconButton
|
||||||
docsLink
|
docsLink
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
ErrorService,
|
ErrorService,
|
||||||
i18nKey,
|
i18nKey,
|
||||||
i18nPipe,
|
i18nPipe,
|
||||||
|
i18nService,
|
||||||
LoadingService,
|
LoadingService,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
@@ -24,6 +25,7 @@ export class ControlsService {
|
|||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
private readonly i18n = inject(i18nPipe)
|
private readonly i18n = inject(i18nPipe)
|
||||||
|
private readonly i18nService = inject(i18nService)
|
||||||
|
|
||||||
async start({ title, alerts, id }: T.Manifest, unmet: boolean) {
|
async start({ title, alerts, id }: T.Manifest, unmet: boolean) {
|
||||||
const deps =
|
const deps =
|
||||||
@@ -31,7 +33,7 @@ export class ControlsService {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
(unmet && !(await this.alert(deps))) ||
|
(unmet && !(await this.alert(deps))) ||
|
||||||
(alerts.start && !(await this.alert(alerts.start as i18nKey)))
|
(alerts.start && !(await this.alert(alerts.start)))
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -49,7 +51,7 @@ export class ControlsService {
|
|||||||
|
|
||||||
async stop({ id, title, alerts }: T.Manifest) {
|
async stop({ id, title, alerts }: T.Manifest) {
|
||||||
const depMessage = `${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
|
const depMessage = `${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
|
||||||
let content = alerts.stop || ''
|
let content = alerts.stop ? this.i18nService.localize(alerts.stop) : ''
|
||||||
|
|
||||||
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
|
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
|
||||||
content = content ? `${content}.\n\n${depMessage}` : depMessage
|
content = content ? `${content}.\n\n${depMessage}` : depMessage
|
||||||
@@ -113,14 +115,14 @@ export class ControlsService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private alert(content: i18nKey): Promise<boolean> {
|
private alert(content: T.LocaleString): Promise<boolean> {
|
||||||
return firstValueFrom(
|
return firstValueFrom(
|
||||||
this.dialog
|
this.dialog
|
||||||
.openConfirm({
|
.openConfirm({
|
||||||
label: 'Warning',
|
label: 'Warning',
|
||||||
size: 's',
|
size: 's',
|
||||||
data: {
|
data: {
|
||||||
content,
|
content: this.i18nService.localize(content),
|
||||||
yes: 'Continue',
|
yes: 'Continue',
|
||||||
no: 'Cancel',
|
no: 'Cancel',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function getInstalledBaseStatus(statusInfo: T.StatusInfo): BaseStatus {
|
|||||||
(!statusInfo.started ||
|
(!statusInfo.started ||
|
||||||
Object.values(statusInfo.health)
|
Object.values(statusInfo.health)
|
||||||
.filter(h => !!h)
|
.filter(h => !!h)
|
||||||
.some(h => h.result === 'starting'))
|
.some(h => h.result === 'starting' || h.result === 'waiting'))
|
||||||
) {
|
) {
|
||||||
return 'starting'
|
return 'starting'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ErrorService,
|
ErrorService,
|
||||||
i18nKey,
|
i18nKey,
|
||||||
i18nPipe,
|
i18nPipe,
|
||||||
|
i18nService,
|
||||||
LoadingService,
|
LoadingService,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
@@ -27,6 +28,7 @@ export class StandardActionsService {
|
|||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
private readonly router = inject(Router)
|
private readonly router = inject(Router)
|
||||||
private readonly i18n = inject(i18nPipe)
|
private readonly i18n = inject(i18nPipe)
|
||||||
|
private readonly i18nService = inject(i18nService)
|
||||||
|
|
||||||
async rebuild(id: string) {
|
async rebuild(id: string) {
|
||||||
const loader = this.loader.open('Rebuilding container').subscribe()
|
const loader = this.loader.open('Rebuilding container').subscribe()
|
||||||
@@ -50,11 +52,12 @@ export class StandardActionsService {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let content = soft
|
let content = soft
|
||||||
? ''
|
? ''
|
||||||
: alerts.uninstall ||
|
: alerts.uninstall
|
||||||
`${this.i18n.transform('Uninstalling')} ${title} ${this.i18n.transform('will permanently delete its data.')}`
|
? this.i18nService.localize(alerts.uninstall)
|
||||||
|
: `${this.i18n.transform('Uninstalling')} ${title} ${this.i18n.transform('will permanently delete its data.')}`
|
||||||
|
|
||||||
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
|
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
|
||||||
content = `${content}${content ? ' ' : ''}${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
|
content = `${content ? `${content} ` : ''}${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
|
|||||||
Reference in New Issue
Block a user