Compare commits
510 Commits
next/minor
...
sdk-commen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ba55860dd | ||
|
|
3974c09369 | ||
|
|
f0b41a3a4c | ||
|
|
86ecc4cc99 | ||
|
|
d1162272f0 | ||
|
|
13ac469ed0 | ||
|
|
5294e8f444 | ||
|
|
b7da7cd59f | ||
|
|
c196f250f6 | ||
|
|
bee8a0f9d8 | ||
|
|
8213e45b85 | ||
|
|
e9b9925c0e | ||
|
|
0724989792 | ||
|
|
804560d43c | ||
|
|
31352a72c3 | ||
|
|
c7a4f0f9cb | ||
|
|
7879668c40 | ||
|
|
6a01b5eab1 | ||
|
|
80cb2d9ba5 | ||
|
|
8c1a452742 | ||
|
|
135afd0251 | ||
|
|
35f3274f29 | ||
|
|
9af5b87c92 | ||
|
|
66b5bc1897 | ||
|
|
7909941b70 | ||
|
|
4527046f2e | ||
|
|
5a292e6e2a | ||
|
|
84149be3c1 | ||
|
|
d562466fc4 | ||
|
|
9c3053f103 | ||
|
|
dce975410f | ||
|
|
783ce4b3b6 | ||
|
|
675a03bdc5 | ||
|
|
485fced691 | ||
|
|
a22707c1cb | ||
|
|
74e10ec473 | ||
|
|
e25e0f0c12 | ||
|
|
4cae00cb33 | ||
|
|
313b2df540 | ||
|
|
5fbc73755d | ||
|
|
bc4478b0b9 | ||
|
|
68141112b7 | ||
|
|
ccafb599a6 | ||
|
|
52272feb3e | ||
|
|
1abad93646 | ||
|
|
c9468dda02 | ||
|
|
6a1b1627c5 | ||
|
|
cfbace1d91 | ||
|
|
d97ab59bab | ||
|
|
3518eccc87 | ||
|
|
2f19188dae | ||
|
|
3a63f3b840 | ||
|
|
098d9275f4 | ||
|
|
d5c74bc22e | ||
|
|
49d4da03ca | ||
|
|
3765465618 | ||
|
|
61f820d09e | ||
|
|
db7f3341ac | ||
|
|
4decf9335c | ||
|
|
339e5f799a | ||
|
|
89d3e0cf35 | ||
|
|
638ed27599 | ||
|
|
da75b8498e | ||
|
|
8ef4ecf5ac | ||
|
|
0260c1532d | ||
|
|
2a54625f43 | ||
|
|
4e638fb58e | ||
|
|
73274ef6e0 | ||
|
|
e1915bf497 | ||
|
|
8204074bdf | ||
|
|
2ee403e7de | ||
|
|
1974dfd66f | ||
|
|
2e03a95e47 | ||
|
|
b6262c8e13 | ||
|
|
ba740a9ee2 | ||
|
|
8f809dab21 | ||
|
|
c0b2cbe1c8 | ||
|
|
f2142f0bb3 | ||
|
|
86ca23c093 | ||
|
|
463b6ca4ef | ||
|
|
58e0b166cb | ||
|
|
2a678bb017 | ||
|
|
5664456b77 | ||
|
|
3685b7e57e | ||
|
|
989d5f73b1 | ||
|
|
4f84073cb5 | ||
|
|
c190295c34 | ||
|
|
60875644a1 | ||
|
|
113b09ad01 | ||
|
|
2605d0e671 | ||
|
|
d232b91d31 | ||
|
|
c65db31fd9 | ||
|
|
99871805bd | ||
|
|
e8ef39adad | ||
|
|
466b9217b5 | ||
|
|
c9a7f519b9 | ||
|
|
96ae532879 | ||
|
|
eda08d5b0f | ||
|
|
7c12b58bb5 | ||
|
|
5446c89bc0 | ||
|
|
2d0251e585 | ||
|
|
f41710c892 | ||
|
|
df3f79f282 | ||
|
|
f8df692865 | ||
|
|
0c6d3b188d | ||
|
|
e7a38863ab | ||
|
|
720e0fcdab | ||
|
|
bf8ff84522 | ||
|
|
5a9510238e | ||
|
|
7b3c74179b | ||
|
|
cd70fa4c32 | ||
|
|
83133ced6a | ||
|
|
6c5179a179 | ||
|
|
e33ab39b85 | ||
|
|
9567bcec1b | ||
|
|
550b16dc0b | ||
|
|
5d8331b7f7 | ||
|
|
e35b643e51 | ||
|
|
bc6a92677b | ||
|
|
f52072e6ec | ||
|
|
9c43c43a46 | ||
|
|
0430e0f930 | ||
|
|
b945243d1a | ||
|
|
d8484a8b26 | ||
|
|
3c27499795 | ||
|
|
7c772e873d | ||
|
|
db2fab245e | ||
|
|
a9c9917f1a | ||
|
|
23e2e9e9cc | ||
|
|
2369e92460 | ||
|
|
a53b15f2a3 | ||
|
|
72eb8b1eb6 | ||
|
|
4db54f3b83 | ||
|
|
24eb27f005 | ||
|
|
009d76ea35 | ||
|
|
6e8a425eb1 | ||
|
|
66188d791b | ||
|
|
015ff02d71 | ||
|
|
10bfaf5415 | ||
|
|
e3e0b85e0c | ||
|
|
ad0632892e | ||
|
|
f26791ba39 | ||
|
|
2fbaaebf44 | ||
|
|
edb916338c | ||
|
|
f7e947d37d | ||
|
|
a9e3d1ed75 | ||
|
|
ce97827c42 | ||
|
|
3efec07338 | ||
|
|
68f401bfa3 | ||
|
|
1ea525feaa | ||
|
|
57c4a7527e | ||
|
|
5aa9c045e1 | ||
|
|
6f1900f3bb | ||
|
|
bc62de795e | ||
|
|
c62ca4b183 | ||
|
|
876e5bc683 | ||
|
|
b99f3b73cd | ||
|
|
7eecf29449 | ||
|
|
1d331d7810 | ||
|
|
68414678d8 | ||
|
|
2f6b9dac26 | ||
|
|
d1812d875b | ||
|
|
723dea100f | ||
|
|
c4419ed31f | ||
|
|
754ab86e51 | ||
|
|
04dab532cd | ||
|
|
add01ebc68 | ||
|
|
1cc9a1a30b | ||
|
|
92a1de7500 | ||
|
|
a6fedcff80 | ||
|
|
55eb999305 | ||
|
|
377b7b12ce | ||
|
|
ba2906a42e | ||
|
|
ee27f14be0 | ||
|
|
46c8be63a7 | ||
|
|
7ba66c419a | ||
|
|
340775a593 | ||
|
|
35d2ec8a44 | ||
|
|
2983b9950f | ||
|
|
dbf08a6cf8 | ||
|
|
28f31be36f | ||
|
|
3ec4db0225 | ||
|
|
f5688e077a | ||
|
|
2464d255d5 | ||
|
|
586d950b8c | ||
|
|
e7469388cc | ||
|
|
ab6ca8e16a | ||
|
|
02413a4fac | ||
|
|
05b8dd9ad8 | ||
|
|
29c9419a6e | ||
|
|
90e61989a4 | ||
|
|
b1f9f90fec | ||
|
|
b40849f672 | ||
|
|
44560c8da8 | ||
|
|
46fd01c264 | ||
|
|
100695c262 | ||
|
|
54b5a4ae55 | ||
|
|
ffb252962b | ||
|
|
ae31270e63 | ||
|
|
9b2b54d585 | ||
|
|
e1ccc583a3 | ||
|
|
7750e33f82 | ||
|
|
d2c4741f0b | ||
|
|
c79c4f6bde | ||
|
|
3849d0d1a9 | ||
|
|
8bd71ccd5e | ||
|
|
b731f7fb64 | ||
|
|
cd554f77f3 | ||
|
|
8c977c51ca | ||
|
|
a3252f9671 | ||
|
|
9bc945f76f | ||
|
|
f6b4dfffb6 | ||
|
|
68955c29cb | ||
|
|
97e4d036dc | ||
|
|
0f49f54c29 | ||
|
|
828e13adbb | ||
|
|
e6f0067728 | ||
|
|
5c473eb9cc | ||
|
|
2adf34fbaf | ||
|
|
05dd760388 | ||
|
|
2cf4864078 | ||
|
|
df4c92672f | ||
|
|
5b173315f9 | ||
|
|
c85ea7d8fa | ||
|
|
113154702f | ||
|
|
33ae46f76a | ||
|
|
27272680a2 | ||
|
|
b1621f6b34 | ||
|
|
2c65033c0a | ||
|
|
dcfbaa9243 | ||
|
|
accef65ede | ||
|
|
50755d8ba3 | ||
|
|
47b6509f70 | ||
|
|
89f3fdc05f | ||
|
|
03f8b73627 | ||
|
|
2e6e9635c3 | ||
|
|
6a312e3fdd | ||
|
|
0e8961efe3 | ||
|
|
fc2be42418 | ||
|
|
ab4336cfd7 | ||
|
|
63a29d3a4a | ||
|
|
31856d9895 | ||
|
|
f51dcf23d6 | ||
|
|
1883c9666e | ||
|
|
4b4cf76641 | ||
|
|
495bbecc01 | ||
|
|
e6af7e9885 | ||
|
|
182b8c2283 | ||
|
|
5318cccc5f | ||
|
|
99739575d4 | ||
|
|
6f9069a4fb | ||
|
|
a18ab7f1e9 | ||
|
|
be0371fb11 | ||
|
|
fa3329abf2 | ||
|
|
e830fade06 | ||
|
|
ac392dcb96 | ||
|
|
00a5fdf491 | ||
|
|
7fff9579c0 | ||
|
|
1b006599cf | ||
|
|
ce2842d365 | ||
|
|
7d1096dbd8 | ||
|
|
95722802dc | ||
|
|
95cad7bdd9 | ||
|
|
b2b98643d8 | ||
|
|
bb8109f67d | ||
|
|
e6f02bf8f7 | ||
|
|
57e75e3614 | ||
|
|
89ab67e067 | ||
|
|
115c599fd8 | ||
|
|
3121c08ee8 | ||
|
|
a5bac39196 | ||
|
|
9f640b24b3 | ||
|
|
75e7556bfa | ||
|
|
beb3a9f60a | ||
|
|
dfda2f7d5d | ||
|
|
a77ebd3b55 | ||
|
|
00114287e5 | ||
|
|
a9569d0ed9 | ||
|
|
88d9388be2 | ||
|
|
93c72ecea5 | ||
|
|
b5b0ac50bd | ||
|
|
4d2afdb1a9 | ||
|
|
39a177bd70 | ||
|
|
34fb6ac837 | ||
|
|
f868a454d9 | ||
|
|
751ceab04e | ||
|
|
b6c48d0f98 | ||
|
|
097d77f7b3 | ||
|
|
7a0586684b | ||
|
|
8f34d1c555 | ||
|
|
5270a6781f | ||
|
|
fa93e195cb | ||
|
|
befa9eb16d | ||
|
|
a278c630bb | ||
|
|
76eb0f1775 | ||
|
|
0abe08f243 | ||
|
|
015131f198 | ||
|
|
a730543c76 | ||
|
|
b43ad93c54 | ||
|
|
7850681ce1 | ||
|
|
846189b15b | ||
|
|
657aac0d68 | ||
|
|
81932c8cff | ||
|
|
20f6a5e797 | ||
|
|
949f1c648a | ||
|
|
d159dde2ca | ||
|
|
729a510c5b | ||
|
|
fffc7f4098 | ||
|
|
c7a2e7ada1 | ||
|
|
a2b1968d6e | ||
|
|
398eb13a7f | ||
|
|
956c8a8e03 | ||
|
|
6aba166c82 | ||
|
|
fd7c7ea6b7 | ||
|
|
d85e621bb3 | ||
|
|
25801f374c | ||
|
|
8fd2d0b35c | ||
|
|
dd196c0e11 | ||
|
|
6e2cf8bb3f | ||
|
|
b8eb8a90a5 | ||
|
|
bd4d89fc21 | ||
|
|
6234391229 | ||
|
|
206c185a3b | ||
|
|
7689cbbe0d | ||
|
|
b57a9351b3 | ||
|
|
f0ae9e21ae | ||
|
|
9510c92288 | ||
|
|
755f3f05d8 | ||
|
|
5d8114b475 | ||
|
|
85b39ecf99 | ||
|
|
230838c22b | ||
|
|
a7bfcdcb01 | ||
|
|
47ff630c55 | ||
|
|
70dc53bda7 | ||
|
|
7e1b433c17 | ||
|
|
ec878defab | ||
|
|
1786b70e14 | ||
|
|
7f525fa7dc | ||
|
|
8b89e03999 | ||
|
|
2693b9a42d | ||
|
|
6b336b7b2f | ||
|
|
3c0e77241d | ||
|
|
87461c7f72 | ||
|
|
a67f2b4976 | ||
|
|
8594781780 | ||
|
|
b2c8907635 | ||
|
|
05f4df1a30 | ||
|
|
35fe06a892 | ||
|
|
cd933ce6e4 | ||
|
|
0b93988450 | ||
|
|
12a323f691 | ||
|
|
9c4c211233 | ||
|
|
74ba68ff2c | ||
|
|
7273b37c16 | ||
|
|
0d4ebffc0e | ||
|
|
352b2fb4e7 | ||
|
|
6e6ef57303 | ||
|
|
b80e41503f | ||
|
|
7f28fc17ca | ||
|
|
70d4a0c022 | ||
|
|
8cfd994170 | ||
|
|
641e829e3f | ||
|
|
d202cb731d | ||
|
|
4ab7300376 | ||
|
|
18cc5e0ee8 | ||
|
|
af0cda5dbf | ||
|
|
a730a3719b | ||
|
|
3b669193f6 | ||
|
|
22cd2e3337 | ||
|
|
7e9d453a2c | ||
|
|
a4338b0d03 | ||
|
|
2021431e2f | ||
|
|
5e6a7e134f | ||
|
|
f4fadd366e | ||
|
|
a5b1b4e103 | ||
|
|
7b41b295b7 | ||
|
|
69d5f521a5 | ||
|
|
c0a55142b5 | ||
|
|
513fb3428a | ||
|
|
9a0ae549f6 | ||
|
|
4410d7f195 | ||
|
|
92aa70182d | ||
|
|
90f5864f1e | ||
|
|
e47f126bd5 | ||
|
|
ea6f70e3c5 | ||
|
|
0469aab433 | ||
|
|
ad13b5eb4e | ||
|
|
7324a4973f | ||
|
|
8bc93d23b2 | ||
|
|
c708b685e1 | ||
|
|
cbde91744f | ||
|
|
147e24204b | ||
|
|
13c50e428f | ||
|
|
8403ccd3da | ||
|
|
e92bd61545 | ||
|
|
8215e0221a | ||
|
|
4b44d6fb83 | ||
|
|
0ae3e83ce4 | ||
|
|
f4b573379d | ||
|
|
862ca375ee | ||
|
|
530de6741b | ||
|
|
35c1ff9014 | ||
|
|
3f4caed922 | ||
|
|
09303ab2fb | ||
|
|
df1ac8e1e2 | ||
|
|
7a55c91349 | ||
|
|
c491dfdd3a | ||
|
|
d9cc21f761 | ||
|
|
06207145af | ||
|
|
b195e3435f | ||
|
|
34b4577c0b | ||
|
|
8034e5bbcb | ||
|
|
df7a30bd14 | ||
|
|
d9dfacaaf4 | ||
|
|
d43767b945 | ||
|
|
cb36754c46 | ||
|
|
7e18aafe20 | ||
|
|
f7b079b1b4 | ||
|
|
72ffedead7 | ||
|
|
cf3a501562 | ||
|
|
7becdc3034 | ||
|
|
f0d599781d | ||
|
|
3386105048 | ||
|
|
3b8fb70db1 | ||
|
|
c3ae146580 | ||
|
|
0d079f0d89 | ||
|
|
9f5a90ee9c | ||
|
|
a5307fd8cc | ||
|
|
180589144a | ||
|
|
d9c1867bd7 | ||
|
|
da37d649ec | ||
|
|
4204b4af90 | ||
|
|
941650f668 | ||
|
|
9c0c6c1bd6 | ||
|
|
bd0ddafcd0 | ||
|
|
19f5e92a74 | ||
|
|
3202c38061 | ||
|
|
e35a8c942b | ||
|
|
31811eb91e | ||
|
|
b9316a4112 | ||
|
|
b7abd878ac | ||
|
|
38c2c47789 | ||
|
|
c03778ec8b | ||
|
|
29b0850a94 | ||
|
|
712fde46eb | ||
|
|
c2e79ca5a7 | ||
|
|
c3a52b3989 | ||
|
|
7213d82f1b | ||
|
|
5bcad69cf7 | ||
|
|
c9a487fa4d | ||
|
|
3804a46f3b | ||
|
|
52c0bb5302 | ||
|
|
8aa19e6420 | ||
|
|
4d1c7a3884 | ||
|
|
25f2c057b7 | ||
|
|
010be05920 | ||
|
|
4c465850a2 | ||
|
|
8313dfaeb9 | ||
|
|
873f2b2814 | ||
|
|
e53c90f8f0 | ||
|
|
9499ea8ca9 | ||
|
|
f6c09109ba | ||
|
|
273b5768c4 | ||
|
|
ee13cf7dd9 | ||
|
|
fecbae761e | ||
|
|
e0ee89bdd9 | ||
|
|
833c1f22a3 | ||
|
|
6fed6c8d30 | ||
|
|
94cdaf5314 | ||
|
|
f83ae27352 | ||
|
|
6badf047c3 | ||
|
|
47de9ad15f | ||
|
|
09b91cc663 | ||
|
|
ded16549f7 | ||
|
|
c89e47577b | ||
|
|
bb50beb7ab | ||
|
|
e4cd4d64d7 | ||
|
|
5675fc51a0 | ||
|
|
c7438c4aff | ||
|
|
4a6a3da36c | ||
|
|
a657c332b1 | ||
|
|
cc9cd3fc14 | ||
|
|
234258a077 | ||
|
|
13cda80ee6 | ||
|
|
f6e142baf5 | ||
|
|
ddf1f9bcd5 | ||
|
|
aa950669f6 | ||
|
|
dacd5d3e6b | ||
|
|
e76ccba2f7 | ||
|
|
3933819d53 | ||
|
|
99019c2b1f | ||
|
|
4bf5eb398b | ||
|
|
dbfbac62c0 | ||
|
|
7685293da4 | ||
|
|
ee9c328606 | ||
|
|
cb7790ccba | ||
|
|
6556fcc531 | ||
|
|
178391e7b2 | ||
|
|
18922a1c6d | ||
|
|
5e9e26fa67 | ||
|
|
f5430f9151 | ||
|
|
4dfdf2f92f | ||
|
|
e4d283cc99 | ||
|
|
8ee64d22b3 | ||
|
|
10e3e80042 | ||
|
|
f77a208e2c | ||
|
|
9366dbb96e | ||
|
|
550b17552b | ||
|
|
bec307d0e9 | ||
|
|
93c751f6eb |
1
.claude/settings.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
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 || '');
|
||||||
88
.github/workflows/start-cli.yaml
vendored
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
name: start-cli
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
type: choice
|
||||||
|
description: Environment
|
||||||
|
options:
|
||||||
|
- NONE
|
||||||
|
- dev
|
||||||
|
- unstable
|
||||||
|
- dev-unstable
|
||||||
|
runner:
|
||||||
|
type: choice
|
||||||
|
description: Runner
|
||||||
|
options:
|
||||||
|
- standard
|
||||||
|
- fast
|
||||||
|
arch:
|
||||||
|
type: choice
|
||||||
|
description: Architecture
|
||||||
|
options:
|
||||||
|
- ALL
|
||||||
|
- x86_64
|
||||||
|
- x86_64-apple
|
||||||
|
- aarch64
|
||||||
|
- aarch64-apple
|
||||||
|
- riscv64
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- next/*
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- next/*
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODEJS_VERSION: "24.11.0"
|
||||||
|
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
compile:
|
||||||
|
name: Build Debian Package
|
||||||
|
if: github.event.pull_request.draft != true
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
triple: >-
|
||||||
|
${{
|
||||||
|
fromJson('{
|
||||||
|
"x86_64": ["x86_64-unknown-linux-musl"],
|
||||||
|
"x86_64-apple": ["x86_64-apple-darwin"],
|
||||||
|
"aarch64": ["aarch64-unknown-linux-musl"],
|
||||||
|
"x86_64-apple": ["aarch64-apple-darwin"],
|
||||||
|
"riscv64": ["riscv64gc-unknown-linux-musl"],
|
||||||
|
"ALL": ["x86_64-unknown-linux-musl", "x86_64-apple-darwin", "aarch64-unknown-linux-musl", "aarch64-apple-darwin", "riscv64gc-unknown-linux-musl"]
|
||||||
|
}')[github.event.inputs.platform || 'ALL']
|
||||||
|
}}
|
||||||
|
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
||||||
|
steps:
|
||||||
|
- name: Mount tmpfs
|
||||||
|
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||||
|
run: sudo mount -t tmpfs tmpfs .
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- uses: ./.github/actions/setup-build
|
||||||
|
with:
|
||||||
|
nodejs-version: ${{ env.NODEJS_VERSION }}
|
||||||
|
|
||||||
|
- name: Make
|
||||||
|
run: TARGET=${{ matrix.triple }} make cli
|
||||||
|
env:
|
||||||
|
PLATFORM: ${{ matrix.arch }}
|
||||||
|
SCCACHE_GHA_ENABLED: on
|
||||||
|
SCCACHE_GHA_VERSION: 0
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: start-cli_${{ matrix.triple }}
|
||||||
|
path: core/target/${{ matrix.triple }}/release/start-cli
|
||||||
173
.github/workflows/start-registry.yaml
vendored
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
name: start-registry
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
type: choice
|
||||||
|
description: Environment
|
||||||
|
options:
|
||||||
|
- NONE
|
||||||
|
- dev
|
||||||
|
- unstable
|
||||||
|
- dev-unstable
|
||||||
|
runner:
|
||||||
|
type: choice
|
||||||
|
description: Runner
|
||||||
|
options:
|
||||||
|
- standard
|
||||||
|
- fast
|
||||||
|
arch:
|
||||||
|
type: choice
|
||||||
|
description: Architecture
|
||||||
|
options:
|
||||||
|
- ALL
|
||||||
|
- x86_64
|
||||||
|
- aarch64
|
||||||
|
- riscv64
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- next/*
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- next/*
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODEJS_VERSION: "24.11.0"
|
||||||
|
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
compile:
|
||||||
|
name: Build Debian Package
|
||||||
|
if: github.event.pull_request.draft != true
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
arch: >-
|
||||||
|
${{
|
||||||
|
fromJson('{
|
||||||
|
"x86_64": ["x86_64"],
|
||||||
|
"aarch64": ["aarch64"],
|
||||||
|
"riscv64": ["riscv64"],
|
||||||
|
"ALL": ["x86_64", "aarch64", "riscv64"]
|
||||||
|
}')[github.event.inputs.platform || 'ALL']
|
||||||
|
}}
|
||||||
|
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
||||||
|
steps:
|
||||||
|
- name: Mount tmpfs
|
||||||
|
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||||
|
run: sudo mount -t tmpfs tmpfs .
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- uses: ./.github/actions/setup-build
|
||||||
|
with:
|
||||||
|
nodejs-version: ${{ env.NODEJS_VERSION }}
|
||||||
|
|
||||||
|
- name: Make
|
||||||
|
run: make registry-deb
|
||||||
|
env:
|
||||||
|
PLATFORM: ${{ matrix.arch }}
|
||||||
|
SCCACHE_GHA_ENABLED: on
|
||||||
|
SCCACHE_GHA_VERSION: 0
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: start-registry_${{ matrix.arch }}.deb
|
||||||
|
path: results/start-registry-*_${{ matrix.arch }}.deb
|
||||||
|
|
||||||
|
create-image:
|
||||||
|
name: Create Docker Image
|
||||||
|
needs: [compile]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
||||||
|
steps:
|
||||||
|
- name: Cleaning up unnecessary files
|
||||||
|
run: |
|
||||||
|
sudo apt-get remove --purge -y google-chrome-stable firefox mono-devel
|
||||||
|
sudo apt-get autoremove -y
|
||||||
|
sudo apt-get clean
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
sudo mount -t tmpfs tmpfs .
|
||||||
|
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||||
|
|
||||||
|
- name: Set up docker QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: "Login to GitHub Container Registry"
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{github.actor}}
|
||||||
|
password: ${{secrets.GITHUB_TOKEN}}
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ghcr.io/Start9Labs/startos-registry
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Download debian package
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: start-registry_*.deb
|
||||||
|
|
||||||
|
- name: Map matrix.arch to docker platform
|
||||||
|
run: |
|
||||||
|
platforms=""
|
||||||
|
for deb in *.deb; do
|
||||||
|
filename=$(basename "$deb" .deb)
|
||||||
|
arch="${filename#*_}"
|
||||||
|
case "$arch" in
|
||||||
|
x86_64)
|
||||||
|
platform="linux/amd64"
|
||||||
|
;;
|
||||||
|
aarch64)
|
||||||
|
platform="linux/arm64"
|
||||||
|
;;
|
||||||
|
riscv64)
|
||||||
|
platform="linux/riscv64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown architecture: $arch" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ -z "$platforms" ]; then
|
||||||
|
platforms="$platform"
|
||||||
|
else
|
||||||
|
platforms="$platforms,$platform"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "DOCKER_PLATFORM=$platforms" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
cat | docker buildx build --platform "$DOCKER_PLATFORM" --push -t ${{ steps.meta.outputs.tags }} -f - . << 'EOF'
|
||||||
|
FROM debian:trixie
|
||||||
|
|
||||||
|
ADD *.deb .
|
||||||
|
|
||||||
|
RUN apt-get install -y ./*_$(uname -m).deb && rm *.deb
|
||||||
|
|
||||||
|
VOLUME /var/lib/startos
|
||||||
|
|
||||||
|
ENV RUST_LOG=startos=debug
|
||||||
|
|
||||||
|
ENTRYPOINT ["start-registryd"]
|
||||||
|
|
||||||
|
EOF
|
||||||
84
.github/workflows/start-tunnel.yaml
vendored
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
name: start-tunnel
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
type: choice
|
||||||
|
description: Environment
|
||||||
|
options:
|
||||||
|
- NONE
|
||||||
|
- dev
|
||||||
|
- unstable
|
||||||
|
- dev-unstable
|
||||||
|
runner:
|
||||||
|
type: choice
|
||||||
|
description: Runner
|
||||||
|
options:
|
||||||
|
- standard
|
||||||
|
- fast
|
||||||
|
arch:
|
||||||
|
type: choice
|
||||||
|
description: Architecture
|
||||||
|
options:
|
||||||
|
- ALL
|
||||||
|
- x86_64
|
||||||
|
- aarch64
|
||||||
|
- riscv64
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- next/*
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- next/*
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODEJS_VERSION: "24.11.0"
|
||||||
|
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
compile:
|
||||||
|
name: Build Debian Package
|
||||||
|
if: github.event.pull_request.draft != true
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
arch: >-
|
||||||
|
${{
|
||||||
|
fromJson('{
|
||||||
|
"x86_64": ["x86_64"],
|
||||||
|
"aarch64": ["aarch64"],
|
||||||
|
"riscv64": ["riscv64"],
|
||||||
|
"ALL": ["x86_64", "aarch64", "riscv64"]
|
||||||
|
}')[github.event.inputs.platform || 'ALL']
|
||||||
|
}}
|
||||||
|
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
||||||
|
steps:
|
||||||
|
- name: Mount tmpfs
|
||||||
|
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||||
|
run: sudo mount -t tmpfs tmpfs .
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- uses: ./.github/actions/setup-build
|
||||||
|
with:
|
||||||
|
nodejs-version: ${{ env.NODEJS_VERSION }}
|
||||||
|
|
||||||
|
- name: Make
|
||||||
|
run: make tunnel-deb
|
||||||
|
env:
|
||||||
|
PLATFORM: ${{ matrix.arch }}
|
||||||
|
SCCACHE_GHA_ENABLED: on
|
||||||
|
SCCACHE_GHA_VERSION: 0
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: start-tunnel_${{ matrix.arch }}.deb
|
||||||
|
path: results/start-tunnel-*_${{ matrix.arch }}.deb
|
||||||
165
.github/workflows/startos-iso.yaml
vendored
@@ -27,7 +27,8 @@ on:
|
|||||||
- x86_64-nonfree
|
- x86_64-nonfree
|
||||||
- aarch64
|
- aarch64
|
||||||
- aarch64-nonfree
|
- aarch64-nonfree
|
||||||
- raspberrypi
|
# - raspberrypi
|
||||||
|
- riscv64
|
||||||
deploy:
|
deploy:
|
||||||
type: choice
|
type: choice
|
||||||
description: Deploy
|
description: Deploy
|
||||||
@@ -44,13 +45,18 @@ on:
|
|||||||
- master
|
- master
|
||||||
- next/*
|
- next/*
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
NODEJS_VERSION: "20.16.0"
|
NODEJS_VERSION: "24.11.0"
|
||||||
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
|
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
|
||||||
|
|
||||||
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:
|
||||||
@@ -62,39 +68,45 @@ jobs:
|
|||||||
"aarch64": ["aarch64"],
|
"aarch64": ["aarch64"],
|
||||||
"aarch64-nonfree": ["aarch64"],
|
"aarch64-nonfree": ["aarch64"],
|
||||||
"raspberrypi": ["aarch64"],
|
"raspberrypi": ["aarch64"],
|
||||||
"ALL": ["x86_64", "aarch64"]
|
"riscv64": ["riscv64"],
|
||||||
|
"ALL": ["x86_64", "aarch64", "riscv64"]
|
||||||
}')[github.event.inputs.platform || 'ALL']
|
}')[github.event.inputs.platform || 'ALL']
|
||||||
}}
|
}}
|
||||||
runs-on: ${{ fromJson('["ubuntu-22.04", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
runs-on: >-
|
||||||
|
${{
|
||||||
|
fromJson(
|
||||||
|
format(
|
||||||
|
'["{0}", "{1}"]',
|
||||||
|
fromJson('{
|
||||||
|
"x86_64": "ubuntu-latest",
|
||||||
|
"aarch64": "ubuntu-24.04-arm",
|
||||||
|
"riscv64": "ubuntu-latest"
|
||||||
|
}')[matrix.arch],
|
||||||
|
fromJson('{
|
||||||
|
"x86_64": "buildjet-32vcpu-ubuntu-2204",
|
||||||
|
"aarch64": "buildjet-32vcpu-ubuntu-2204-arm",
|
||||||
|
"riscv64": "buildjet-32vcpu-ubuntu-2204"
|
||||||
|
}')[matrix.arch]
|
||||||
|
)
|
||||||
|
)[github.event.inputs.runner == 'fast']
|
||||||
|
}}
|
||||||
steps:
|
steps:
|
||||||
- run: |
|
- name: Mount tmpfs
|
||||||
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 system dependencies
|
|
||||||
run: sudo apt-get update && sudo apt-get install -y qemu-user-static systemd-container squashfuse
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Make
|
- name: Make
|
||||||
run: make ARCH=${{ matrix.arch }} compiled-${{ matrix.arch }}.tar
|
run: make ARCH=${{ matrix.arch }} compiled-${{ matrix.arch }}.tar
|
||||||
|
env:
|
||||||
|
SCCACHE_GHA_ENABLED: on
|
||||||
|
SCCACHE_GHA_VERSION: 0
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -106,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", "raspberrypi"]
|
["x86_64", "x86_64-nonfree", "aarch64", "aarch64-nonfree", "riscv64"]
|
||||||
]',
|
]',
|
||||||
github.event.inputs.platform || 'ALL'
|
github.event.inputs.platform || 'ALL'
|
||||||
)
|
)
|
||||||
@@ -122,13 +135,22 @@ jobs:
|
|||||||
${{
|
${{
|
||||||
fromJson(
|
fromJson(
|
||||||
format(
|
format(
|
||||||
'["ubuntu-22.04", "{0}"]',
|
'["{0}", "{1}"]',
|
||||||
|
fromJson('{
|
||||||
|
"x86_64": "ubuntu-latest",
|
||||||
|
"x86_64-nonfree": "ubuntu-latest",
|
||||||
|
"aarch64": "ubuntu-24.04-arm",
|
||||||
|
"aarch64-nonfree": "ubuntu-24.04-arm",
|
||||||
|
"raspberrypi": "ubuntu-24.04-arm",
|
||||||
|
"riscv64": "ubuntu-24.04-arm",
|
||||||
|
}')[matrix.platform],
|
||||||
fromJson('{
|
fromJson('{
|
||||||
"x86_64": "buildjet-8vcpu-ubuntu-2204",
|
"x86_64": "buildjet-8vcpu-ubuntu-2204",
|
||||||
"x86_64-nonfree": "buildjet-8vcpu-ubuntu-2204",
|
"x86_64-nonfree": "buildjet-8vcpu-ubuntu-2204",
|
||||||
"aarch64": "buildjet-8vcpu-ubuntu-2204-arm",
|
"aarch64": "buildjet-8vcpu-ubuntu-2204-arm",
|
||||||
"aarch64-nonfree": "buildjet-8vcpu-ubuntu-2204-arm",
|
"aarch64-nonfree": "buildjet-8vcpu-ubuntu-2204-arm",
|
||||||
"raspberrypi": "buildjet-8vcpu-ubuntu-2204-arm",
|
"raspberrypi": "buildjet-8vcpu-ubuntu-2204-arm",
|
||||||
|
"riscv64": "buildjet-8vcpu-ubuntu-2204",
|
||||||
}')[matrix.platform]
|
}')[matrix.platform]
|
||||||
)
|
)
|
||||||
)[github.event.inputs.runner == 'fast']
|
)[github.event.inputs.runner == 'fast']
|
||||||
@@ -142,39 +164,42 @@ jobs:
|
|||||||
"aarch64": "aarch64",
|
"aarch64": "aarch64",
|
||||||
"aarch64-nonfree": "aarch64",
|
"aarch64-nonfree": "aarch64",
|
||||||
"raspberrypi": "aarch64",
|
"raspberrypi": "aarch64",
|
||||||
|
"riscv64": "riscv64",
|
||||||
}')[matrix.platform]
|
}')[matrix.platform]
|
||||||
}}
|
}}
|
||||||
steps:
|
steps:
|
||||||
- name: Free space
|
- name: Free space
|
||||||
run: rm -rf /opt/hostedtoolcache*
|
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.)
|
||||||
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
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.x"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y qemu-user-static
|
|
||||||
wget https://deb.debian.org/debian/pool/main/d/debspawn/debspawn_0.6.2-1_all.deb
|
|
||||||
sha256sum ./debspawn_0.6.2-1_all.deb | grep 37ef27458cb1e35e8bce4d4f639b06b4b3866fc0b9191ec6b9bd157afd06a817
|
|
||||||
sudo apt-get install -y ./debspawn_0.6.2-1_all.deb
|
|
||||||
|
|
||||||
- name: Configure debspawn
|
|
||||||
run: |
|
|
||||||
sudo mkdir -p /etc/debspawn/
|
|
||||||
echo "AllowUnsafePermissions=true" | sudo tee /etc/debspawn/global.toml
|
|
||||||
sudo mkdir -p /var/tmp/debspawn
|
|
||||||
|
|
||||||
- run: sudo mount -t tmpfs tmpfs /var/tmp/debspawn
|
|
||||||
if: ${{ github.event.inputs.runner == 'fast' && (matrix.platform == 'x86_64' || matrix.platform == 'x86_64-nonfree') }}
|
|
||||||
|
|
||||||
- name: Download compiled artifacts
|
- name: Download compiled artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -187,22 +212,19 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p web/node_modules
|
mkdir -p web/node_modules
|
||||||
mkdir -p web/dist/raw
|
mkdir -p web/dist/raw
|
||||||
mkdir -p core/startos/bindings
|
mkdir -p core/bindings
|
||||||
mkdir -p sdk/base/lib/osBindings
|
mkdir -p sdk/base/lib/osBindings
|
||||||
mkdir -p container-runtime/node_modules
|
mkdir -p container-runtime/node_modules
|
||||||
mkdir -p container-runtime/dist
|
mkdir -p container-runtime/dist
|
||||||
mkdir -p container-runtime/dist/node_modules
|
mkdir -p container-runtime/dist/node_modules
|
||||||
mkdir -p core/startos/bindings
|
|
||||||
mkdir -p sdk/dist
|
mkdir -p sdk/dist
|
||||||
mkdir -p sdk/baseDist
|
mkdir -p sdk/baseDist
|
||||||
mkdir -p patch-db/client/node_modules
|
mkdir -p patch-db/client/node_modules
|
||||||
mkdir -p patch-db/client/dist
|
mkdir -p patch-db/client/dist
|
||||||
mkdir -p web/.angular
|
mkdir -p web/.angular
|
||||||
mkdir -p web/dist/raw/ui
|
mkdir -p web/dist/raw/ui
|
||||||
mkdir -p web/dist/raw/install-wizard
|
|
||||||
mkdir -p web/dist/raw/setup-wizard
|
mkdir -p web/dist/raw/setup-wizard
|
||||||
mkdir -p web/dist/static/ui
|
mkdir -p web/dist/static/ui
|
||||||
mkdir -p web/dist/static/install-wizard
|
|
||||||
mkdir -p web/dist/static/setup-wizard
|
mkdir -p web/dist/static/setup-wizard
|
||||||
PLATFORM=${{ matrix.platform }} make -t compiled-${{ env.ARCH }}.tar
|
PLATFORM=${{ matrix.platform }} make -t compiled-${{ env.ARCH }}.tar
|
||||||
|
|
||||||
@@ -232,40 +254,3 @@ jobs:
|
|||||||
name: ${{ matrix.platform }}.img
|
name: ${{ matrix.platform }}.img
|
||||||
path: results/*.img
|
path: results/*.img
|
||||||
if: ${{ matrix.platform == 'raspberrypi' }}
|
if: ${{ matrix.platform == 'raspberrypi' }}
|
||||||
|
|
||||||
- name: Upload OTA to registry
|
|
||||||
run: >-
|
|
||||||
PLATFORM=${{ matrix.platform }} make upload-ota TARGET="${{
|
|
||||||
fromJson('{
|
|
||||||
"alpha": "alpha-registry-x.start9.com",
|
|
||||||
"beta": "beta-registry.start9.com",
|
|
||||||
}')[github.event.inputs.deploy]
|
|
||||||
}}" KEY="${{
|
|
||||||
fromJson(
|
|
||||||
format('{{
|
|
||||||
"alpha": "{0}",
|
|
||||||
"beta": "{1}",
|
|
||||||
}}', secrets.ALPHA_INDEX_KEY, secrets.BETA_INDEX_KEY)
|
|
||||||
)[github.event.inputs.deploy]
|
|
||||||
}}"
|
|
||||||
if: ${{ github.event.inputs.deploy != '' && github.event.inputs.deploy != 'NONE' }}
|
|
||||||
|
|
||||||
index:
|
|
||||||
if: ${{ github.event.inputs.deploy != '' && github.event.inputs.deploy != 'NONE' }}
|
|
||||||
needs: [image]
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- run: >-
|
|
||||||
curl "https://${{
|
|
||||||
fromJson('{
|
|
||||||
"alpha": "alpha-registry-x.start9.com",
|
|
||||||
"beta": "beta-registry.start9.com",
|
|
||||||
}')[github.event.inputs.deploy]
|
|
||||||
}}:8443/resync.cgi?key=${{
|
|
||||||
fromJson(
|
|
||||||
format('{{
|
|
||||||
"alpha": "{0}",
|
|
||||||
"beta": "{1}",
|
|
||||||
}}', secrets.ALPHA_INDEX_KEY, secrets.BETA_INDEX_KEY)
|
|
||||||
)[github.event.inputs.deploy]
|
|
||||||
}}"
|
|
||||||
|
|||||||
17
.github/workflows/test.yaml
vendored
@@ -10,22 +10,29 @@ on:
|
|||||||
- master
|
- master
|
||||||
- next/*
|
- next/*
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
NODEJS_VERSION: "20.16.0"
|
NODEJS_VERSION: "24.11.0"
|
||||||
ENVIRONMENT: dev-unstable
|
ENVIRONMENT: dev-unstable
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Run Automated Tests
|
name: Run Automated Tests
|
||||||
runs-on: ubuntu-22.04
|
if: github.event.pull_request.draft != true
|
||||||
|
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
|
||||||
|
|||||||
29
.gitignore
vendored
@@ -1,31 +1,24 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
system-images/binfmt/binfmt.tar
|
*.img
|
||||||
system-images/compat/compat.tar
|
*.img.gz
|
||||||
system-images/util/util.tar
|
*.img.xz
|
||||||
/*.img
|
*.zip
|
||||||
/*.img.gz
|
|
||||||
/*.img.xz
|
|
||||||
/*-raspios-bullseye-arm64-lite.img
|
|
||||||
/*-raspios-bullseye-arm64-lite.zip
|
|
||||||
/product_key.txt
|
/product_key.txt
|
||||||
/*_product_key.txt
|
/*_product_key.txt
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
deploy_web.sh
|
deploy_web.sh
|
||||||
deploy_web.sh
|
|
||||||
secrets.db
|
secrets.db
|
||||||
.vscode/
|
.vscode/
|
||||||
/cargo-deps/**/*
|
/build/env/*.txt
|
||||||
/PLATFORM.txt
|
*.deb
|
||||||
/ENVIRONMENT.txt
|
|
||||||
/GIT_HASH.txt
|
|
||||||
/VERSION.txt
|
|
||||||
/*.deb
|
|
||||||
/target
|
/target
|
||||||
/*.squashfs
|
*.squashfs
|
||||||
/results
|
/results
|
||||||
/dpkg-workdir
|
/dpkg-workdir
|
||||||
/compiled.tar
|
/compiled.tar
|
||||||
/compiled-*.tar
|
/compiled-*.tar
|
||||||
/firmware
|
/build/lib/firmware
|
||||||
/tmp
|
tmp
|
||||||
|
web/.i18n-checked
|
||||||
|
docs/USER.md
|
||||||
|
|||||||
101
ARCHITECTURE.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
StartOS is an open-source Linux distribution for running personal servers. It manages discovery, installation, network configuration, backups, and health monitoring of self-hosted services.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- Backend: Rust (async/Tokio, Axum web framework)
|
||||||
|
- Frontend: Angular 20 + TypeScript + TaigaUI
|
||||||
|
- Container runtime: Node.js/TypeScript with LXC
|
||||||
|
- Database/State: Patch-DB (git submodule) - storage layer with reactive frontend sync
|
||||||
|
- API: JSON-RPC via rpc-toolkit (see `core/rpc-toolkit.md`)
|
||||||
|
- Auth: Password + session cookie, public/private key signatures, local authcookie (see `core/src/middleware/auth/`)
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/
|
||||||
|
├── assets/ # Screenshots for README
|
||||||
|
├── build/ # Auxiliary files and scripts for deployed images
|
||||||
|
├── container-runtime/ # Node.js program managing package containers
|
||||||
|
├── core/ # Rust backend: API, daemon (startd), CLI (start-cli)
|
||||||
|
├── debian/ # Debian package maintainer scripts
|
||||||
|
├── image-recipe/ # Scripts for building StartOS images
|
||||||
|
├── patch-db/ # (submodule) Diff-based data store for frontend sync
|
||||||
|
├── sdk/ # TypeScript SDK for building StartOS packages
|
||||||
|
└── web/ # Web UIs (Angular)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
- **`core/`** — Rust backend daemon. Produces a single binary `startbox` that is symlinked as `startd` (main daemon), `start-cli` (CLI), `start-container` (runs inside LXC containers), `registrybox` (package registry), and `tunnelbox` (VPN/tunnel). Handles all backend logic: RPC API, service lifecycle, networking (DNS, ACME, WiFi, Tor, WireGuard), backups, and database state management. See [core/ARCHITECTURE.md](core/ARCHITECTURE.md).
|
||||||
|
|
||||||
|
- **`web/`** — Angular 20 + TypeScript workspace using Taiga UI. Contains three applications (admin UI, setup wizard, VPN management) and two shared libraries (common components/services, marketplace). Communicates with the backend exclusively via JSON-RPC. See [web/ARCHITECTURE.md](web/ARCHITECTURE.md).
|
||||||
|
|
||||||
|
- **`container-runtime/`** — Node.js runtime that runs inside each service's LXC container. Loads the service's JavaScript from its S9PK package and manages subcontainers. Communicates with the host daemon via JSON-RPC over Unix socket. See [container-runtime/CLAUDE.md](container-runtime/CLAUDE.md).
|
||||||
|
|
||||||
|
- **`sdk/`** — TypeScript SDK for packaging services for StartOS (`@start9labs/start-sdk`). Split into `base/` (core types, ABI definitions, effects interface, consumed by web as `@start9labs/start-sdk-base`) and `package/` (full SDK for service developers, consumed by container-runtime as `@start9labs/start-sdk`).
|
||||||
|
|
||||||
|
- **`patch-db/`** — Git submodule providing diff-based state synchronization. Uses CBOR encoding. Backend mutations produce diffs that are pushed to the frontend via WebSocket, enabling reactive UI updates without polling. See [patch-db repo](https://github.com/Start9Labs/patch-db).
|
||||||
|
|
||||||
|
## Build Pipeline
|
||||||
|
|
||||||
|
Components have a strict dependency chain. Changes flow in one direction:
|
||||||
|
|
||||||
|
```
|
||||||
|
Rust (core/)
|
||||||
|
→ cargo test exports ts-rs types to core/bindings/
|
||||||
|
→ rsync copies to sdk/base/lib/osBindings/
|
||||||
|
→ SDK build produces baseDist/ and dist/
|
||||||
|
→ web/ consumes baseDist/ (via @start9labs/start-sdk-base)
|
||||||
|
→ container-runtime/ consumes dist/ (via @start9labs/start-sdk)
|
||||||
|
```
|
||||||
|
|
||||||
|
Key make targets along this chain:
|
||||||
|
|
||||||
|
| Step | Command | What it does |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `cargo check -p start-os` | Verify Rust compiles |
|
||||||
|
| 2 | `make ts-bindings` | Export ts-rs types → rsync to SDK |
|
||||||
|
| 3 | `cd sdk && make baseDist dist` | Build SDK packages |
|
||||||
|
| 4 | `cd web && npm run check` | Type-check Angular projects |
|
||||||
|
| 5 | `cd container-runtime && npm run check` | Type-check runtime |
|
||||||
|
|
||||||
|
**Important**: Editing `sdk/base/lib/osBindings/*.ts` alone is NOT sufficient — you must rebuild the SDK bundle (step 3) before web/container-runtime can see the changes.
|
||||||
|
|
||||||
|
## Cross-Layer Verification
|
||||||
|
|
||||||
|
When making changes across multiple layers (Rust, SDK, web, container-runtime), verify in this order:
|
||||||
|
|
||||||
|
1. **Rust**: `cargo check -p start-os` — verifies core compiles
|
||||||
|
2. **TS bindings**: `make ts-bindings` — regenerates TypeScript types from Rust `#[ts(export)]` structs
|
||||||
|
- Runs `./core/build/build-ts.sh` to export ts-rs types to `core/bindings/`
|
||||||
|
- Syncs `core/bindings/` → `sdk/base/lib/osBindings/` via rsync
|
||||||
|
- If you manually edit files in `sdk/base/lib/osBindings/`, you must still rebuild the SDK (step 3)
|
||||||
|
3. **SDK bundle**: `cd sdk && make baseDist dist` — compiles SDK source into packages
|
||||||
|
- `baseDist/` is consumed by `/web` (via `@start9labs/start-sdk-base`)
|
||||||
|
- `dist/` is consumed by `/container-runtime` (via `@start9labs/start-sdk`)
|
||||||
|
- Web and container-runtime reference the **built** SDK, not source files
|
||||||
|
4. **Web type check**: `cd web && npm run check` — type-checks all Angular projects
|
||||||
|
5. **Container runtime type check**: `cd container-runtime && npm run check` — type-checks the runtime
|
||||||
|
|
||||||
|
## Data Flow: Backend to Frontend
|
||||||
|
|
||||||
|
StartOS uses Patch-DB for reactive state synchronization:
|
||||||
|
|
||||||
|
1. The backend mutates state via `db.mutate()`, producing CBOR diffs
|
||||||
|
2. Diffs are pushed to the frontend over a persistent WebSocket connection
|
||||||
|
3. The frontend applies diffs to its local state copy and notifies observers
|
||||||
|
4. Components watch specific database paths via `PatchDB.watch$()`, receiving updates reactively
|
||||||
|
|
||||||
|
This means the UI is always eventually consistent with the backend — after any mutating API call, the frontend waits for the corresponding PatchDB diff before resolving, so the UI reflects the result immediately.
|
||||||
|
|
||||||
|
## Further Reading
|
||||||
|
|
||||||
|
- [core/ARCHITECTURE.md](core/ARCHITECTURE.md) — Rust backend architecture
|
||||||
|
- [web/ARCHITECTURE.md](web/ARCHITECTURE.md) — Angular frontend architecture
|
||||||
|
- [container-runtime/CLAUDE.md](container-runtime/CLAUDE.md) — Container runtime details
|
||||||
|
- [core/rpc-toolkit.md](core/rpc-toolkit.md) — JSON-RPC handler patterns
|
||||||
|
- [core/s9pk-structure.md](core/s9pk-structure.md) — S9PK package format
|
||||||
|
- [docs/exver.md](docs/exver.md) — Extended versioning format
|
||||||
|
- [docs/VERSION_BUMP.md](docs/VERSION_BUMP.md) — Version bumping guide
|
||||||
55
CLAUDE.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
See [ARCHITECTURE.md](ARCHITECTURE.md) for the full system architecture, component map, build pipeline, and cross-layer verification order.
|
||||||
|
|
||||||
|
Each major component has its own `CLAUDE.md` with detailed guidance: `core/`, `web/`, `container-runtime/`, `sdk/`.
|
||||||
|
|
||||||
|
## Build & Development
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for:
|
||||||
|
- Environment setup and requirements
|
||||||
|
- Build commands and make targets
|
||||||
|
- Testing and formatting commands
|
||||||
|
- Environment variables
|
||||||
|
|
||||||
|
**Quick reference:**
|
||||||
|
```bash
|
||||||
|
. ./devmode.sh # Enable dev mode
|
||||||
|
make update-startbox REMOTE=start9@<ip> # Fastest iteration (binary + UI)
|
||||||
|
make test-core # Run Rust tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Operating Rules
|
||||||
|
|
||||||
|
- Always verify cross-layer changes using the order described in [ARCHITECTURE.md](ARCHITECTURE.md#cross-layer-verification)
|
||||||
|
- Check component-level CLAUDE.md files for component-specific conventions. ALWAYS read it before operating on that component.
|
||||||
|
- Follow existing patterns before inventing new ones
|
||||||
|
|
||||||
|
## Supplementary Documentation
|
||||||
|
|
||||||
|
The `docs/` directory contains cross-cutting documentation for AI assistants:
|
||||||
|
|
||||||
|
- `TODO.md` - Pending tasks for AI agents (check this first, remove items when completed)
|
||||||
|
- `USER.md` - Current user identifier (gitignored, see below)
|
||||||
|
- `exver.md` - Extended versioning format (used across core, sdk, and web)
|
||||||
|
- `VERSION_BUMP.md` - Guide for bumping the StartOS version across the codebase
|
||||||
|
|
||||||
|
Component-specific docs live alongside their code (e.g., `core/rpc-toolkit.md`, `core/i18n-patterns.md`).
|
||||||
|
|
||||||
|
### Session Startup
|
||||||
|
|
||||||
|
On startup:
|
||||||
|
|
||||||
|
1. **Check for `docs/USER.md`** - If it doesn't exist, prompt the user for their name/identifier and create it. This file is gitignored since it varies per developer.
|
||||||
|
|
||||||
|
2. **Check `docs/TODO.md` for relevant tasks** - Show TODOs that either:
|
||||||
|
- Have no `@username` tag (relevant to everyone)
|
||||||
|
- Are tagged with the current user's identifier
|
||||||
|
|
||||||
|
Skip TODOs tagged with a different user.
|
||||||
|
|
||||||
|
3. **Ask "What would you like to do today?"** - Offer options for each relevant TODO item, plus "Something else" for other requests.
|
||||||
311
CONTRIBUTING.md
@@ -1,119 +1,240 @@
|
|||||||
# Contributing to StartOS
|
# Contributing to StartOS
|
||||||
|
|
||||||
This guide is for contributing to the StartOS. If you are interested in packaging a service for StartOS, visit the [service packaging guide](https://docs.start9.com/latest/developer-docs/). If you are interested in promoting, providing technical support, creating tutorials, or helping in other ways, please visit the [Start9 website](https://start9.com/contribute).
|
This guide is for contributing to the StartOS. If you are interested in packaging a service for StartOS, visit the [service packaging guide](https://github.com/Start9Labs/ai-service-packaging). If you are interested in promoting, providing technical support, creating tutorials, or helping in other ways, please visit the [Start9 website](https://start9.com/contribute).
|
||||||
|
|
||||||
|
|
||||||
## Collaboration
|
## Collaboration
|
||||||
|
|
||||||
- [Matrix](https://matrix.to/#/#community-dev:matrix.start9labs.com)
|
- [Matrix](https://matrix.to/#/#dev-startos:matrix.start9labs.com)
|
||||||
- [Telegram](https://t.me/start9_labs/47471)
|
|
||||||
|
|
||||||
## Project Structure
|
For project structure and system architecture, see [ARCHITECTURE.md](ARCHITECTURE.md).
|
||||||
|
|
||||||
```bash
|
|
||||||
/
|
|
||||||
├── assets/
|
|
||||||
├── core/
|
|
||||||
├── build/
|
|
||||||
├── debian/
|
|
||||||
├── web/
|
|
||||||
├── image-recipe/
|
|
||||||
├── patch-db
|
|
||||||
└── system-images/
|
|
||||||
```
|
|
||||||
#### assets
|
|
||||||
screenshots for the StartOS README
|
|
||||||
|
|
||||||
#### core
|
|
||||||
An API, daemon (startd), CLI (start-cli), and SDK (start-sdk) that together provide the core functionality of StartOS.
|
|
||||||
|
|
||||||
#### build
|
|
||||||
Auxiliary files and scripts to include in deployed StartOS images
|
|
||||||
|
|
||||||
#### debian
|
|
||||||
Maintainer scripts for the StartOS Debian package
|
|
||||||
|
|
||||||
#### web
|
|
||||||
Web UIs served under various conditions and used to interact with StartOS APIs.
|
|
||||||
|
|
||||||
#### image-recipe
|
|
||||||
Scripts for building StartOS images
|
|
||||||
|
|
||||||
#### patch-db (submodule)
|
|
||||||
A diff based data store used to synchronize data between the web interfaces and server.
|
|
||||||
|
|
||||||
#### system-images
|
|
||||||
Docker images that assist with creating backups.
|
|
||||||
|
|
||||||
## Environment Setup
|
## Environment Setup
|
||||||
|
|
||||||
#### Clone the StartOS repository
|
### Installing Dependencies (Debian/Ubuntu)
|
||||||
|
|
||||||
|
> Debian/Ubuntu is the only officially supported build environment.
|
||||||
|
> MacOS has limited build capabilities and Windows requires [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/Start9Labs/start-os.git
|
sudo apt update
|
||||||
|
sudo apt install -y ca-certificates curl gpg build-essential
|
||||||
|
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||||
|
echo "deb [arch=$(dpkg-architecture -q DEB_HOST_ARCH) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian bookworm stable" | sudo tee /etc/apt/sources.list.d/docker.list
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y sed grep gawk jq gzip brotli containerd.io docker-ce docker-ce-cli docker-compose-plugin qemu-user-static binfmt-support squashfs-tools git debspawn rsync b3sum
|
||||||
|
sudo mkdir -p /etc/debspawn/
|
||||||
|
echo "AllowUnsafePermissions=true" | sudo tee /etc/debspawn/global.toml
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
sudo su $USER
|
||||||
|
docker run --privileged --rm tonistiigi/binfmt --install all
|
||||||
|
docker buildx create --use
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # proceed with default installation
|
||||||
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
|
||||||
|
source ~/.bashrc
|
||||||
|
nvm install 24
|
||||||
|
nvm use 24
|
||||||
|
nvm alias default 24 # this prevents your machine from reverting back to another version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cloning the Repository
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone --recursive https://github.com/Start9Labs/start-os.git --branch next/major
|
||||||
cd start-os
|
cd start-os
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Load the PatchDB submodule
|
### Development Mode
|
||||||
|
|
||||||
|
For faster iteration during development:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git submodule update --init --recursive
|
. ./devmode.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Continue to your project of interest for additional instructions:
|
This sets `ENVIRONMENT=dev` and `GIT_BRANCH_AS_HASH=1` to prevent rebuilds on every commit.
|
||||||
- [`core`](core/README.md)
|
|
||||||
- [`web-interfaces`](web-interfaces/README.md)
|
|
||||||
- [`build`](build/README.md)
|
|
||||||
- [`patch-db`](https://github.com/Start9Labs/patch-db)
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
This project uses [GNU Make](https://www.gnu.org/software/make/) to build its components. To build any specific component, simply run `make <TARGET>` replacing `<TARGET>` with the name of the target you'd like to build
|
|
||||||
|
All builds can be performed on any operating system that can run Docker.
|
||||||
|
|
||||||
|
This project uses [GNU Make](https://www.gnu.org/software/make/) to build its components.
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- [GNU Make](https://www.gnu.org/software/make/)
|
- [GNU Make](https://www.gnu.org/software/make/)
|
||||||
- [Docker](https://docs.docker.com/get-docker/)
|
- [Docker](https://docs.docker.com/get-docker/) or [Podman](https://podman.io/)
|
||||||
- [NodeJS v18.15.0](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
|
- [NodeJS v20.16.0](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
|
||||||
- [sed](https://www.gnu.org/software/sed/)
|
- [Rust](https://rustup.rs/) (nightly for formatting)
|
||||||
- [grep](https://www.gnu.org/software/grep/)
|
- [sed](https://www.gnu.org/software/sed/), [grep](https://www.gnu.org/software/grep/), [awk](https://www.gnu.org/software/gawk/)
|
||||||
- [awk](https://www.gnu.org/software/gawk/)
|
|
||||||
- [jq](https://jqlang.github.io/jq/)
|
- [jq](https://jqlang.github.io/jq/)
|
||||||
- [gzip](https://www.gnu.org/software/gzip/)
|
- [gzip](https://www.gnu.org/software/gzip/), [brotli](https://github.com/google/brotli)
|
||||||
- [brotli](https://github.com/google/brotli)
|
|
||||||
|
|
||||||
### Environment variables
|
### Environment Variables
|
||||||
- `PLATFORM`: which platform you would like to build for. Must be one of `x86_64`, `x86_64-nonfree`, `aarch64`, `aarch64-nonfree`, `raspberrypi`
|
|
||||||
- NOTE: `nonfree` images are for including `nonfree` firmware packages in the built ISO
|
|
||||||
- `ENVIRONMENT`: a hyphen separated set of feature flags to enable
|
|
||||||
- `dev`: enables password ssh (INSECURE!) and does not compress frontends
|
|
||||||
- `unstable`: enables assertions that will cause errors on unexpected inconsistencies that are undesirable in production use either for performance or reliability reasons
|
|
||||||
- `docker`: use `docker` instead of `podman`
|
|
||||||
- `GIT_BRANCH_AS_HASH`: set to `1` to use the current git branch name as the git hash so that the project does not need to be rebuilt on each commit
|
|
||||||
|
|
||||||
### Useful Make Targets
|
| Variable | Description |
|
||||||
- `iso`: Create a full `.iso` image
|
| -------------------- | --------------------------------------------------------------------------------------------------- |
|
||||||
- Only possible from Debian
|
| `PLATFORM` | Target platform: `x86_64`, `x86_64-nonfree`, `aarch64`, `aarch64-nonfree`, `riscv64`, `raspberrypi` |
|
||||||
- Not available for `PLATFORM=raspberrypi`
|
| `ENVIRONMENT` | Hyphen-separated feature flags (see below) |
|
||||||
- Additional Requirements:
|
| `PROFILE` | Build profile: `release` (default) or `dev` |
|
||||||
- [debspawn](https://github.com/lkhq/debspawn)
|
| `GIT_BRANCH_AS_HASH` | Set to `1` to use git branch name as version hash (avoids rebuilds) |
|
||||||
- `img`: Create a full `.img` image
|
|
||||||
- Only possible from Debian
|
**ENVIRONMENT flags:**
|
||||||
- Only available for `PLATFORM=raspberrypi`
|
|
||||||
- Additional Requirements:
|
- `dev` - Enables password SSH before setup, skips frontend compression
|
||||||
- [debspawn](https://github.com/lkhq/debspawn)
|
- `unstable` - Enables assertions and debugging with performance penalty
|
||||||
- `format`: Run automatic code formatting for the project
|
- `console` - Enables tokio-console for async debugging
|
||||||
- Additional Requirements:
|
|
||||||
- [rust](https://rustup.rs/)
|
**Platform notes:**
|
||||||
- `test`: Run automated tests for the project
|
|
||||||
- Additional Requirements:
|
- `-nonfree` variants include proprietary firmware and drivers
|
||||||
- [rust](https://rustup.rs/)
|
- `raspberrypi` includes non-free components by necessity
|
||||||
- `update`: Deploy the current working project to a device over ssh as if through an over-the-air update
|
- Platform is remembered between builds if not specified
|
||||||
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
|
|
||||||
- `reflash`: Deploy the current working project to a device over ssh as if using a live `iso` image to reflash it
|
### Make Targets
|
||||||
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
|
|
||||||
- `update-overlay`: Deploy the current working project to a device over ssh to the in-memory overlay without restarting it
|
#### Building
|
||||||
- WARNING: changes will be reverted after the device is rebooted
|
|
||||||
- WARNING: changes to `init` will not take effect as the device is already initialized
|
| Target | Description |
|
||||||
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
|
| ------------- | ---------------------------------------------- |
|
||||||
- `wormhole`: Deploy the `startbox` to a device using [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole)
|
| `iso` | Create full `.iso` image (not for raspberrypi) |
|
||||||
- When the build it complete will emit a command to paste into the shell of the device to upgrade it
|
| `img` | Create full `.img` image (raspberrypi only) |
|
||||||
- Additional Requirements:
|
| `deb` | Build Debian package |
|
||||||
- [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole)
|
| `all` | Build all Rust binaries |
|
||||||
- `clean`: Delete all compiled artifacts
|
| `uis` | Build all web UIs |
|
||||||
|
| `ui` | Build main UI only |
|
||||||
|
| `ts-bindings` | Generate TypeScript bindings from Rust types |
|
||||||
|
|
||||||
|
#### Deploying to Device
|
||||||
|
|
||||||
|
For devices on the same network:
|
||||||
|
|
||||||
|
| Target | Description |
|
||||||
|
| ------------------------------------ | ----------------------------------------------- |
|
||||||
|
| `update-startbox REMOTE=start9@<ip>` | Deploy binary + UI only (fastest) |
|
||||||
|
| `update-deb REMOTE=start9@<ip>` | Deploy full Debian package |
|
||||||
|
| `update REMOTE=start9@<ip>` | OTA-style update |
|
||||||
|
| `reflash REMOTE=start9@<ip>` | Reflash as if using live ISO |
|
||||||
|
| `update-overlay REMOTE=start9@<ip>` | Deploy to in-memory overlay (reverts on reboot) |
|
||||||
|
|
||||||
|
For devices on different networks (uses [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole)):
|
||||||
|
|
||||||
|
| Target | Description |
|
||||||
|
| ------------------- | -------------------- |
|
||||||
|
| `wormhole` | Send startbox binary |
|
||||||
|
| `wormhole-deb` | Send Debian package |
|
||||||
|
| `wormhole-squashfs` | Send squashfs image |
|
||||||
|
|
||||||
|
### Creating a VM
|
||||||
|
|
||||||
|
Install virt-manager:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y virt-manager
|
||||||
|
sudo usermod -aG libvirt $USER
|
||||||
|
sudo su $USER
|
||||||
|
virt-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow the screenshot walkthrough in [`assets/create-vm/`](assets/create-vm/) to create a new virtual machine. Key steps:
|
||||||
|
|
||||||
|
1. Create a new virtual machine
|
||||||
|
2. Browse for the ISO — create a storage pool pointing to your `results/` directory
|
||||||
|
3. Select "Generic or unknown OS"
|
||||||
|
4. Set memory and CPUs
|
||||||
|
5. Create a disk and name the VM
|
||||||
|
|
||||||
|
Build an ISO first:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
PLATFORM=$(uname -m) ENVIRONMENT=dev make iso
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Other
|
||||||
|
|
||||||
|
| Target | Description |
|
||||||
|
| ------------------------ | ------------------------------------------- |
|
||||||
|
| `format` | Run code formatting (Rust nightly required) |
|
||||||
|
| `test` | Run all automated tests |
|
||||||
|
| `test-core` | Run Rust tests |
|
||||||
|
| `test-sdk` | Run SDK tests |
|
||||||
|
| `test-container-runtime` | Run container runtime tests |
|
||||||
|
| `clean` | Delete all compiled artifacts |
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test # All tests
|
||||||
|
make test-core # Rust tests (via ./core/run-tests.sh)
|
||||||
|
make test-sdk # SDK tests
|
||||||
|
make test-container-runtime # Container runtime tests
|
||||||
|
|
||||||
|
# Run specific Rust test
|
||||||
|
cd core && cargo test <test_name> --features=test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Formatting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rust (requires nightly)
|
||||||
|
make format
|
||||||
|
|
||||||
|
# TypeScript/HTML/SCSS (web)
|
||||||
|
cd web && npm run format
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
### Formatting
|
||||||
|
|
||||||
|
Run the formatters before committing. Configuration is handled by `rustfmt.toml` (Rust) and prettier configs (TypeScript).
|
||||||
|
|
||||||
|
### Documentation & Comments
|
||||||
|
|
||||||
|
**Rust:**
|
||||||
|
|
||||||
|
- Add doc comments (`///`) to public APIs, structs, and non-obvious functions
|
||||||
|
- Use `//` comments sparingly for complex logic that isn't self-evident
|
||||||
|
- Prefer self-documenting code (clear naming, small functions) over comments
|
||||||
|
|
||||||
|
**TypeScript:**
|
||||||
|
|
||||||
|
- Document exported functions and complex types with JSDoc
|
||||||
|
- Keep comments focused on "why" rather than "what"
|
||||||
|
|
||||||
|
**General:**
|
||||||
|
|
||||||
|
- Don't add comments that just restate the code
|
||||||
|
- Update or remove comments when code changes
|
||||||
|
- TODOs should include context: `// TODO(username): reason`
|
||||||
|
|
||||||
|
### Commit Messages
|
||||||
|
|
||||||
|
Use [Conventional Commits](https://www.conventionalcommits.org/):
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>(<scope>): <description>
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
[optional footer]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Types:**
|
||||||
|
|
||||||
|
- `feat` - New feature
|
||||||
|
- `fix` - Bug fix
|
||||||
|
- `docs` - Documentation only
|
||||||
|
- `style` - Formatting, no code change
|
||||||
|
- `refactor` - Code change that neither fixes a bug nor adds a feature
|
||||||
|
- `test` - Adding or updating tests
|
||||||
|
- `chore` - Build process, dependencies, etc.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```
|
||||||
|
feat(web): add dark mode toggle
|
||||||
|
fix(core): resolve race condition in service startup
|
||||||
|
docs: update CONTRIBUTING.md with style guidelines
|
||||||
|
refactor(sdk): simplify package validation logic
|
||||||
|
```
|
||||||
|
|||||||
134
DEVELOPMENT.md
@@ -1,134 +0,0 @@
|
|||||||
# Setting up your development environment on Debian/Ubuntu
|
|
||||||
|
|
||||||
A step-by-step guide
|
|
||||||
|
|
||||||
> This is the only officially supported build environment.
|
|
||||||
> MacOS has limited build capabilities and Windows requires [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install)
|
|
||||||
|
|
||||||
## Installing dependencies
|
|
||||||
|
|
||||||
Run the following commands one at a time
|
|
||||||
|
|
||||||
```sh
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y ca-certificates curl gpg build-essential
|
|
||||||
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
|
||||||
echo "deb [arch=$(dpkg-architecture -q DEB_HOST_ARCH) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian bookworm stable" | sudo tee /etc/apt/sources.list.d/docker.list
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y sed grep gawk jq gzip brotli containerd.io docker-ce docker-ce-cli docker-compose-plugin qemu-user-static binfmt-support squashfs-tools git debspawn rsync b3sum
|
|
||||||
sudo mkdir -p /etc/debspawn/
|
|
||||||
echo "AllowUnsafePermissions=true" | sudo tee /etc/debspawn/global.toml
|
|
||||||
sudo usermod -aG docker $USER
|
|
||||||
sudo su $USER
|
|
||||||
docker run --privileged --rm tonistiigi/binfmt --install all
|
|
||||||
docker buildx create --use
|
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # proceed with default installation
|
|
||||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
|
|
||||||
source ~/.bashrc
|
|
||||||
nvm install 20
|
|
||||||
nvm use 20
|
|
||||||
nvm alias default 20 # this prevents your machine from reverting back to another version
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cloning the repository
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git clone --recursive https://github.com/Start9Labs/start-os.git --branch next/minor
|
|
||||||
cd start-os
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building an ISO
|
|
||||||
|
|
||||||
```sh
|
|
||||||
PLATFORM=$(uname -m) ENVIRONMENT=dev make iso
|
|
||||||
```
|
|
||||||
|
|
||||||
This will build an ISO for your current architecture. If you are building to run on an architecture other than the one you are currently on, replace `$(uname -m)` with the correct platform for the device (one of `aarch64`, `aarch64-nonfree`, `x86_64`, `x86_64-nonfree`, `raspberrypi`)
|
|
||||||
|
|
||||||
## Creating a VM
|
|
||||||
|
|
||||||
### Install virt-manager
|
|
||||||
|
|
||||||
```sh
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y virt-manager
|
|
||||||
sudo usermod -aG libvirt $USER
|
|
||||||
sudo su $USER
|
|
||||||
```
|
|
||||||
|
|
||||||
### Launch virt-manager
|
|
||||||
|
|
||||||
```sh
|
|
||||||
virt-manager
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create new virtual machine
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
#### make sure to set "Target Path" to the path to your results directory in start-os
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
## Updating a VM
|
|
||||||
|
|
||||||
The fastest way to update a VM to your latest code depends on what you changed:
|
|
||||||
|
|
||||||
### UI or startd:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
PLATFORM=$(uname -m) ENVIRONMENT=dev make update-startbox REMOTE=start9@<VM IP>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Container runtime or debian dependencies:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
PLATFORM=$(uname -m) ENVIRONMENT=dev make update-deb REMOTE=start9@<VM IP>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Image recipe:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
PLATFORM=$(uname -m) ENVIRONMENT=dev make update-squashfs REMOTE=start9@<VM IP>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
If the device you are building for is not available via ssh, it is also possible to use `magic-wormhole` to send the relevant files.
|
|
||||||
|
|
||||||
### Prerequisites:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y magic-wormhole
|
|
||||||
```
|
|
||||||
|
|
||||||
As before, the fastest way to update a VM to your latest code depends on what you changed. Each of the following commands will return a command to paste into the shell of the device you would like to upgrade.
|
|
||||||
|
|
||||||
### UI or startd:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
PLATFORM=$(uname -m) ENVIRONMENT=dev make wormhole
|
|
||||||
```
|
|
||||||
|
|
||||||
### Container runtime or debian dependencies:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
PLATFORM=$(uname -m) ENVIRONMENT=dev make wormhole-deb
|
|
||||||
```
|
|
||||||
|
|
||||||
### Image recipe:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
PLATFORM=$(uname -m) ENVIRONMENT=dev make wormhole-squashfs
|
|
||||||
```
|
|
||||||
318
Makefile
@@ -1,32 +1,44 @@
|
|||||||
PLATFORM_FILE := $(shell ./check-platform.sh)
|
ls-files = $(shell git ls-files --cached --others --exclude-standard $1)
|
||||||
ENVIRONMENT_FILE := $(shell ./check-environment.sh)
|
PROFILE = release
|
||||||
GIT_HASH_FILE := $(shell ./check-git-hash.sh)
|
|
||||||
VERSION_FILE := $(shell ./check-version.sh)
|
PLATFORM_FILE := $(shell ./build/env/check-platform.sh)
|
||||||
BASENAME := $(shell ./basename.sh)
|
ENVIRONMENT_FILE := $(shell ./build/env/check-environment.sh)
|
||||||
PLATFORM := $(shell if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi)
|
GIT_HASH_FILE := $(shell ./build/env/check-git-hash.sh)
|
||||||
|
VERSION_FILE := $(shell ./build/env/check-version.sh)
|
||||||
|
BASENAME := $(shell PROJECT=startos ./build/env/basename.sh)
|
||||||
|
PLATFORM := $(shell if [ -f $(PLATFORM_FILE) ]; then cat $(PLATFORM_FILE); else echo unknown; fi)
|
||||||
ARCH := $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo aarch64; else echo $(PLATFORM) | sed 's/-nonfree$$//g'; fi)
|
ARCH := $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo aarch64; else echo $(PLATFORM) | sed 's/-nonfree$$//g'; fi)
|
||||||
|
RUST_ARCH := $(shell if [ "$(ARCH)" = "riscv64" ]; then echo riscv64gc; else echo $(ARCH); fi)
|
||||||
|
REGISTRY_BASENAME := $(shell PROJECT=start-registry PLATFORM=$(ARCH) ./build/env/basename.sh)
|
||||||
|
TUNNEL_BASENAME := $(shell PROJECT=start-tunnel PLATFORM=$(ARCH) ./build/env/basename.sh)
|
||||||
IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo iso; fi)
|
IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo iso; fi)
|
||||||
WEB_UIS := web/dist/raw/ui/index.html web/dist/raw/setup-wizard/index.html web/dist/raw/install-wizard/index.html
|
WEB_UIS := web/dist/raw/ui/index.html web/dist/raw/setup-wizard/index.html
|
||||||
COMPRESSED_WEB_UIS := web/dist/static/ui/index.html web/dist/static/setup-wizard/index.html web/dist/static/install-wizard/index.html
|
COMPRESSED_WEB_UIS := web/dist/static/ui/index.html web/dist/static/setup-wizard/index.html
|
||||||
FIRMWARE_ROMS := ./firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json)
|
FIRMWARE_ROMS := build/lib/firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./build/lib/firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json)
|
||||||
BUILD_SRC := $(shell git ls-files build) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS)
|
BUILD_SRC := $(call ls-files, build/lib) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS)
|
||||||
DEBIAN_SRC := $(shell git ls-files debian/)
|
IMAGE_RECIPE_SRC := $(call ls-files, build/image-recipe/)
|
||||||
IMAGE_RECIPE_SRC := $(shell git ls-files image-recipe/)
|
STARTD_SRC := core/startd.service $(BUILD_SRC)
|
||||||
STARTD_SRC := core/startos/startd.service $(BUILD_SRC)
|
CORE_SRC := $(call ls-files, core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE)
|
||||||
COMPAT_SRC := $(shell git ls-files system-images/compat/)
|
WEB_SHARED_SRC := $(call ls-files, web/projects/shared) $(call ls-files, web/projects/marketplace) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist/index.js sdk/baseDist/package.json web/patchdb-ui-seed.json sdk/dist/package.json
|
||||||
UTILS_SRC := $(shell git ls-files system-images/utils/)
|
WEB_UI_SRC := $(call ls-files, web/projects/ui)
|
||||||
BINFMT_SRC := $(shell git ls-files system-images/binfmt/)
|
WEB_SETUP_WIZARD_SRC := $(call ls-files, web/projects/setup-wizard)
|
||||||
CORE_SRC := $(shell git ls-files core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE)
|
WEB_START_TUNNEL_SRC := $(call ls-files, web/projects/start-tunnel)
|
||||||
WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist/index.js sdk/baseDist/package.json web/patchdb-ui-seed.json sdk/dist/package.json
|
|
||||||
WEB_UI_SRC := $(shell git ls-files web/projects/ui)
|
|
||||||
WEB_SETUP_WIZARD_SRC := $(shell git ls-files web/projects/setup-wizard)
|
|
||||||
WEB_INSTALL_WIZARD_SRC := $(shell git ls-files web/projects/install-wizard)
|
|
||||||
PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client)
|
PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client)
|
||||||
GZIP_BIN := $(shell which pigz || which gzip)
|
GZIP_BIN := $(shell which pigz || which gzip)
|
||||||
TAR_BIN := $(shell which gtar || which tar)
|
TAR_BIN := $(shell which gtar || which tar)
|
||||||
COMPILED_TARGETS := core/target/$(ARCH)-unknown-linux-musl/release/startbox core/target/$(ARCH)-unknown-linux-musl/release/containerbox system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar container-runtime/rootfs.$(ARCH).squashfs
|
COMPILED_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox core/target/$(RUST_ARCH)-unknown-linux-musl/release/start-container container-runtime/rootfs.$(ARCH).squashfs
|
||||||
ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; fi') $(PLATFORM_FILE)
|
STARTOS_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) target/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs $(PLATFORM_FILE) \
|
||||||
REBUILD_TYPES = 1
|
$(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then \
|
||||||
|
echo target/aarch64-unknown-linux-musl/release/pi-beep; \
|
||||||
|
fi) \
|
||||||
|
$(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then \
|
||||||
|
echo target/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph; \
|
||||||
|
fi') \
|
||||||
|
$(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)console($$|-) ]]; then \
|
||||||
|
echo target/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console; \
|
||||||
|
fi')
|
||||||
|
REGISTRY_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox core/start-registryd.service
|
||||||
|
TUNNEL_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/start-tunneld.service
|
||||||
|
|
||||||
ifeq ($(REMOTE),)
|
ifeq ($(REMOTE),)
|
||||||
mkdir = mkdir -p $1
|
mkdir = mkdir -p $1
|
||||||
@@ -49,23 +61,18 @@ endif
|
|||||||
|
|
||||||
.DELETE_ON_ERROR:
|
.DELETE_ON_ERROR:
|
||||||
|
|
||||||
.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs sudo wormhole wormhole-deb test test-core test-sdk test-container-runtime registry
|
.PHONY: all metadata install clean format install-cli cli uis ui reflash deb $(IMAGE_TYPE) squashfs wormhole wormhole-deb test test-core test-sdk test-container-runtime registry install-registry tunnel install-tunnel ts-bindings
|
||||||
|
|
||||||
all: $(ALL_TARGETS)
|
all: $(STARTOS_TARGETS)
|
||||||
|
|
||||||
touch:
|
touch:
|
||||||
touch $(ALL_TARGETS)
|
touch $(STARTOS_TARGETS)
|
||||||
|
|
||||||
metadata: $(VERSION_FILE) $(PLATFORM_FILE) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE)
|
metadata: $(VERSION_FILE) $(PLATFORM_FILE) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE)
|
||||||
|
|
||||||
sudo:
|
|
||||||
sudo true
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f system-images/**/*.tar
|
|
||||||
rm -rf system-images/compat/target
|
|
||||||
rm -rf core/target
|
rm -rf core/target
|
||||||
rm -rf core/startos/bindings
|
rm -rf core/bindings
|
||||||
rm -rf web/.angular
|
rm -rf web/.angular
|
||||||
rm -f web/config.json
|
rm -f web/config.json
|
||||||
rm -rf web/node_modules
|
rm -rf web/node_modules
|
||||||
@@ -73,7 +80,7 @@ clean:
|
|||||||
rm -rf patch-db/client/node_modules
|
rm -rf patch-db/client/node_modules
|
||||||
rm -rf patch-db/client/dist
|
rm -rf patch-db/client/dist
|
||||||
rm -rf patch-db/target
|
rm -rf patch-db/target
|
||||||
rm -rf cargo-deps
|
rm -rf target
|
||||||
rm -rf dpkg-workdir
|
rm -rf dpkg-workdir
|
||||||
rm -rf image-recipe/deb
|
rm -rf image-recipe/deb
|
||||||
rm -rf results
|
rm -rf results
|
||||||
@@ -81,14 +88,8 @@ clean:
|
|||||||
rm -rf container-runtime/dist
|
rm -rf container-runtime/dist
|
||||||
rm -rf container-runtime/node_modules
|
rm -rf container-runtime/node_modules
|
||||||
rm -f container-runtime/*.squashfs
|
rm -f container-runtime/*.squashfs
|
||||||
if [ -d container-runtime/tmp/combined ] && mountpoint container-runtime/tmp/combined; then sudo umount container-runtime/tmp/combined; fi
|
|
||||||
if [ -d container-runtime/tmp/lower ] && mountpoint container-runtime/tmp/lower; then sudo umount container-runtime/tmp/lower; fi
|
|
||||||
rm -rf container-runtime/tmp
|
|
||||||
(cd sdk && make clean)
|
(cd sdk && make clean)
|
||||||
rm -f ENVIRONMENT.txt
|
rm -f env/*.txt
|
||||||
rm -f PLATFORM.txt
|
|
||||||
rm -f GIT_HASH.txt
|
|
||||||
rm -f VERSION.txt
|
|
||||||
|
|
||||||
format:
|
format:
|
||||||
cd core && cargo +nightly fmt
|
cd core && cargo +nightly fmt
|
||||||
@@ -98,48 +99,95 @@ test: | test-core test-sdk test-container-runtime
|
|||||||
test-core: $(CORE_SRC) $(ENVIRONMENT_FILE)
|
test-core: $(CORE_SRC) $(ENVIRONMENT_FILE)
|
||||||
./core/run-tests.sh
|
./core/run-tests.sh
|
||||||
|
|
||||||
test-sdk: $(shell git ls-files sdk) sdk/base/lib/osBindings/index.ts
|
test-sdk: $(call ls-files, sdk) sdk/base/lib/osBindings/index.ts
|
||||||
cd sdk && make test
|
cd sdk && make test
|
||||||
|
|
||||||
test-container-runtime: container-runtime/node_modules/.package-lock.json $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json
|
test-container-runtime: container-runtime/node_modules/.package-lock.json $(call ls-files, container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json
|
||||||
cd container-runtime && npm test
|
cd container-runtime && npm test
|
||||||
|
|
||||||
cli:
|
install-cli: $(GIT_HASH_FILE)
|
||||||
cd core && ./install-cli.sh
|
./core/build/build-cli.sh --install
|
||||||
|
|
||||||
registry:
|
cli: $(GIT_HASH_FILE)
|
||||||
cd core && ./build-registrybox.sh
|
./core/build/build-cli.sh
|
||||||
|
|
||||||
|
registry: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox
|
||||||
|
|
||||||
|
install-registry: $(REGISTRY_TARGETS)
|
||||||
|
$(call mkdir,$(DESTDIR)/usr/bin)
|
||||||
|
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox,$(DESTDIR)/usr/bin/start-registrybox)
|
||||||
|
$(call ln,/usr/bin/start-registrybox,$(DESTDIR)/usr/bin/start-registryd)
|
||||||
|
$(call ln,/usr/bin/start-registrybox,$(DESTDIR)/usr/bin/start-registry)
|
||||||
|
|
||||||
|
$(call mkdir,$(DESTDIR)/lib/systemd/system)
|
||||||
|
$(call cp,core/start-registryd.service,$(DESTDIR)/lib/systemd/system/start-registryd.service)
|
||||||
|
|
||||||
|
core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox: $(CORE_SRC) $(ENVIRONMENT_FILE)
|
||||||
|
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build/build-registrybox.sh
|
||||||
|
|
||||||
|
tunnel: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox
|
||||||
|
|
||||||
|
install-tunnel: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/start-tunneld.service
|
||||||
|
$(call mkdir,$(DESTDIR)/usr/bin)
|
||||||
|
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox,$(DESTDIR)/usr/bin/start-tunnelbox)
|
||||||
|
$(call ln,/usr/bin/start-tunnelbox,$(DESTDIR)/usr/bin/start-tunneld)
|
||||||
|
$(call ln,/usr/bin/start-tunnelbox,$(DESTDIR)/usr/bin/start-tunnel)
|
||||||
|
|
||||||
|
$(call mkdir,$(DESTDIR)/lib/systemd/system)
|
||||||
|
$(call cp,core/start-tunneld.service,$(DESTDIR)/lib/systemd/system/start-tunneld.service)
|
||||||
|
|
||||||
|
$(call mkdir,$(DESTDIR)/usr/lib/startos/scripts)
|
||||||
|
$(call cp,build/lib/scripts/forward-port,$(DESTDIR)/usr/lib/startos/scripts/forward-port)
|
||||||
|
|
||||||
|
$(call mkdir,$(DESTDIR)/etc/apt/sources.list.d)
|
||||||
|
$(call cp,apt/start9.list,$(DESTDIR)/etc/apt/sources.list.d/start9.list)
|
||||||
|
$(call mkdir,$(DESTDIR)/usr/share/keyrings)
|
||||||
|
$(call cp,apt/start9.gpg,$(DESTDIR)/usr/share/keyrings/start9.gpg)
|
||||||
|
|
||||||
|
core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox: $(CORE_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) web/dist/static/start-tunnel/index.html
|
||||||
|
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build/build-tunnelbox.sh
|
||||||
|
|
||||||
deb: results/$(BASENAME).deb
|
deb: results/$(BASENAME).deb
|
||||||
|
|
||||||
debian/control: build/lib/depends build/lib/conflicts
|
results/$(BASENAME).deb: debian/dpkg-build.sh $(call ls-files,debian/startos) $(STARTOS_TARGETS)
|
||||||
./debuild/control.sh
|
PLATFORM=$(PLATFORM) REQUIRES=debian ./build/os-compat/run-compat.sh ./debian/dpkg-build.sh
|
||||||
|
|
||||||
results/$(BASENAME).deb: dpkg-build.sh $(DEBIAN_SRC) $(ALL_TARGETS)
|
registry-deb: results/$(REGISTRY_BASENAME).deb
|
||||||
PLATFORM=$(PLATFORM) ./dpkg-build.sh
|
|
||||||
|
results/$(REGISTRY_BASENAME).deb: debian/dpkg-build.sh $(call ls-files,debian/start-registry) $(REGISTRY_TARGETS)
|
||||||
|
PROJECT=start-registry PLATFORM=$(ARCH) REQUIRES=debian ./build/os-compat/run-compat.sh ./debian/dpkg-build.sh
|
||||||
|
|
||||||
|
tunnel-deb: results/$(TUNNEL_BASENAME).deb
|
||||||
|
|
||||||
|
results/$(TUNNEL_BASENAME).deb: debian/dpkg-build.sh $(call ls-files,debian/start-tunnel) $(TUNNEL_TARGETS) build/lib/scripts/forward-port
|
||||||
|
PROJECT=start-tunnel PLATFORM=$(ARCH) REQUIRES=debian DEPENDS=wireguard-tools,iptables,conntrack ./build/os-compat/run-compat.sh ./debian/dpkg-build.sh
|
||||||
|
|
||||||
$(IMAGE_TYPE): results/$(BASENAME).$(IMAGE_TYPE)
|
$(IMAGE_TYPE): results/$(BASENAME).$(IMAGE_TYPE)
|
||||||
|
|
||||||
squashfs: results/$(BASENAME).squashfs
|
squashfs: results/$(BASENAME).squashfs
|
||||||
|
|
||||||
results/$(BASENAME).$(IMAGE_TYPE) results/$(BASENAME).squashfs: $(IMAGE_RECIPE_SRC) results/$(BASENAME).deb
|
results/$(BASENAME).$(IMAGE_TYPE) results/$(BASENAME).squashfs: $(IMAGE_RECIPE_SRC) results/$(BASENAME).deb
|
||||||
./image-recipe/run-local-build.sh "results/$(BASENAME).deb"
|
ARCH=$(ARCH) ./build/image-recipe/run-local-build.sh "results/$(BASENAME).deb"
|
||||||
|
|
||||||
# For creating os images. DO NOT USE
|
# For creating os images. DO NOT USE
|
||||||
install: $(ALL_TARGETS)
|
install: $(STARTOS_TARGETS)
|
||||||
$(call mkdir,$(DESTDIR)/usr/bin)
|
$(call mkdir,$(DESTDIR)/usr/bin)
|
||||||
$(call mkdir,$(DESTDIR)/usr/sbin)
|
$(call mkdir,$(DESTDIR)/usr/sbin)
|
||||||
$(call cp,core/target/$(ARCH)-unknown-linux-musl/release/startbox,$(DESTDIR)/usr/bin/startbox)
|
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox,$(DESTDIR)/usr/bin/startbox)
|
||||||
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd)
|
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd)
|
||||||
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-cli)
|
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-cli)
|
||||||
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-sdk)
|
if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,target/aarch64-unknown-linux-musl/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi
|
||||||
if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-musl/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi
|
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then \
|
||||||
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then $(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); fi
|
$(call cp,target/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph,$(DESTDIR)/usr/bin/flamegraph); \
|
||||||
$(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs,$(DESTDIR)/usr/bin/startos-backup-fs)
|
fi
|
||||||
|
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)console($$|-) ]]'; then \
|
||||||
|
$(call cp,target/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); \
|
||||||
|
fi
|
||||||
|
$(call cp,target/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs,$(DESTDIR)/usr/bin/startos-backup-fs)
|
||||||
$(call ln,/usr/bin/startos-backup-fs,$(DESTDIR)/usr/sbin/mount.backup-fs)
|
$(call ln,/usr/bin/startos-backup-fs,$(DESTDIR)/usr/sbin/mount.backup-fs)
|
||||||
|
|
||||||
$(call mkdir,$(DESTDIR)/lib/systemd/system)
|
$(call mkdir,$(DESTDIR)/lib/systemd/system)
|
||||||
$(call cp,core/startos/startd.service,$(DESTDIR)/lib/systemd/system/startd.service)
|
$(call cp,core/startd.service,$(DESTDIR)/lib/systemd/system/startd.service)
|
||||||
|
|
||||||
$(call mkdir,$(DESTDIR)/usr/lib)
|
$(call mkdir,$(DESTDIR)/usr/lib)
|
||||||
$(call rm,$(DESTDIR)/usr/lib/startos)
|
$(call rm,$(DESTDIR)/usr/lib/startos)
|
||||||
@@ -147,30 +195,24 @@ install: $(ALL_TARGETS)
|
|||||||
$(call mkdir,$(DESTDIR)/usr/lib/startos/container-runtime)
|
$(call mkdir,$(DESTDIR)/usr/lib/startos/container-runtime)
|
||||||
$(call cp,container-runtime/rootfs.$(ARCH).squashfs,$(DESTDIR)/usr/lib/startos/container-runtime/rootfs.squashfs)
|
$(call cp,container-runtime/rootfs.$(ARCH).squashfs,$(DESTDIR)/usr/lib/startos/container-runtime/rootfs.squashfs)
|
||||||
|
|
||||||
$(call cp,PLATFORM.txt,$(DESTDIR)/usr/lib/startos/PLATFORM.txt)
|
$(call cp,build/env/PLATFORM.txt,$(DESTDIR)/usr/lib/startos/PLATFORM.txt)
|
||||||
$(call cp,ENVIRONMENT.txt,$(DESTDIR)/usr/lib/startos/ENVIRONMENT.txt)
|
$(call cp,build/env/ENVIRONMENT.txt,$(DESTDIR)/usr/lib/startos/ENVIRONMENT.txt)
|
||||||
$(call cp,GIT_HASH.txt,$(DESTDIR)/usr/lib/startos/GIT_HASH.txt)
|
$(call cp,build/env/GIT_HASH.txt,$(DESTDIR)/usr/lib/startos/GIT_HASH.txt)
|
||||||
$(call cp,VERSION.txt,$(DESTDIR)/usr/lib/startos/VERSION.txt)
|
$(call cp,build/env/VERSION.txt,$(DESTDIR)/usr/lib/startos/VERSION.txt)
|
||||||
|
|
||||||
$(call mkdir,$(DESTDIR)/usr/lib/startos/system-images)
|
update-overlay: $(STARTOS_TARGETS)
|
||||||
$(call cp,system-images/compat/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/compat.tar)
|
|
||||||
$(call cp,system-images/utils/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/utils.tar)
|
|
||||||
|
|
||||||
$(call cp,firmware/$(PLATFORM),$(DESTDIR)/usr/lib/startos/firmware)
|
|
||||||
|
|
||||||
update-overlay: $(ALL_TARGETS)
|
|
||||||
@echo "\033[33m!!! THIS WILL ONLY REFLASH YOUR DEVICE IN MEMORY !!!\033[0m"
|
@echo "\033[33m!!! THIS WILL ONLY REFLASH YOUR DEVICE IN MEMORY !!!\033[0m"
|
||||||
@echo "\033[33mALL CHANGES WILL BE REVERTED IF YOU RESTART THE DEVICE\033[0m"
|
@echo "\033[33mALL CHANGES WILL BE REVERTED IF YOU RESTART THE DEVICE\033[0m"
|
||||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||||
@if [ "`ssh $(REMOTE) 'cat /usr/lib/startos/VERSION.txt'`" != "`cat ./VERSION.txt`" ]; then >&2 echo "StartOS requires migrations: update-overlay is unavailable." && false; fi
|
@if [ "`ssh $(REMOTE) 'cat /usr/lib/startos/VERSION.txt'`" != "`cat $(VERSION_FILE)`" ]; then >&2 echo "StartOS requires migrations: update-overlay is unavailable." && false; fi
|
||||||
$(call ssh,"sudo systemctl stop startd")
|
$(call ssh,"sudo systemctl stop startd")
|
||||||
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) PLATFORM=$(PLATFORM)
|
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) PLATFORM=$(PLATFORM)
|
||||||
$(call ssh,"sudo systemctl start startd")
|
$(call ssh,"sudo systemctl start startd")
|
||||||
|
|
||||||
wormhole: core/target/$(ARCH)-unknown-linux-musl/release/startbox
|
wormhole: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox
|
||||||
@echo "Paste the following command into the shell of your StartOS server:"
|
@echo "Paste the following command into the shell of your StartOS server:"
|
||||||
@echo
|
@echo
|
||||||
@wormhole send core/target/$(ARCH)-unknown-linux-musl/release/startbox 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade \"cd /usr/bin && rm startbox && wormhole receive --accept-file %s && chmod +x startbox\"\n", $$3 }'
|
@wormhole send core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade \"cd /usr/bin && rm startbox && wormhole receive --accept-file %s && chmod +x startbox\"\n", $$3 }'
|
||||||
|
|
||||||
wormhole-deb: results/$(BASENAME).deb
|
wormhole-deb: results/$(BASENAME).deb
|
||||||
@echo "Paste the following command into the shell of your StartOS server:"
|
@echo "Paste the following command into the shell of your StartOS server:"
|
||||||
@@ -182,37 +224,37 @@ wormhole-squashfs: results/$(BASENAME).squashfs
|
|||||||
$(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}'))
|
$(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}'))
|
||||||
@echo "Paste the following command into the shell of your StartOS server:"
|
@echo "Paste the following command into the shell of your StartOS server:"
|
||||||
@echo
|
@echo
|
||||||
@wormhole send results/$(BASENAME).squashfs 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo sh -c '"'"'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE) && cd /media/startos/images && wormhole receive --accept-file %s && mv $(BASENAME).squashfs $(SQFS_SUM).rootfs && ln -rsf ./$(SQFS_SUM).rootfs ../config/current.rootfs && sync && reboot'"'"'\n", $$3 }'
|
@wormhole send results/$(BASENAME).squashfs 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo sh -c '"'"'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE) && /usr/lib/startos/scripts/prune-boot && cd /media/startos/images && wormhole receive --accept-file %s && CHECKSUM=$(SQFS_SUM) /usr/lib/startos/scripts/upgrade ./$(BASENAME).squashfs'"'"'\n", $$3 }'
|
||||||
|
|
||||||
update: $(ALL_TARGETS)
|
update: $(STARTOS_TARGETS)
|
||||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||||
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
|
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
|
||||||
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM)
|
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM)
|
||||||
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y $(shell cat ./build/lib/depends)"')
|
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y $(shell cat ./build/lib/depends)"')
|
||||||
|
|
||||||
update-startbox: core/target/$(ARCH)-unknown-linux-musl/release/startbox # only update binary (faster than full update)
|
update-startbox: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox # only update binary (faster than full update)
|
||||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||||
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
|
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
|
||||||
$(call cp,core/target/$(ARCH)-unknown-linux-musl/release/startbox,/media/startos/next/usr/bin/startbox)
|
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox,/media/startos/next/usr/bin/startbox)
|
||||||
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync true')
|
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync true')
|
||||||
|
|
||||||
update-deb: results/$(BASENAME).deb # better than update, but only available from debian
|
update-deb: results/$(BASENAME).deb # better than update, but only available from debian
|
||||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||||
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
|
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
|
||||||
$(call mkdir,/media/startos/next/tmp/startos-deb)
|
$(call mkdir,/media/startos/next/var/tmp/startos-deb)
|
||||||
$(call cp,results/$(BASENAME).deb,/media/startos/next/tmp/startos-deb/$(BASENAME).deb)
|
$(call cp,results/$(BASENAME).deb,/media/startos/next/var/tmp/startos-deb/$(BASENAME).deb)
|
||||||
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y --reinstall /tmp/startos-deb/$(BASENAME).deb"')
|
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y --reinstall /var/tmp/startos-deb/$(BASENAME).deb"')
|
||||||
|
|
||||||
update-squashfs: results/$(BASENAME).squashfs
|
update-squashfs: results/$(BASENAME).squashfs
|
||||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||||
$(eval SQFS_SUM := $(shell b3sum results/$(BASENAME).squashfs))
|
$(eval SQFS_SUM := $(shell b3sum results/$(BASENAME).squashfs))
|
||||||
$(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}'))
|
$(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}'))
|
||||||
$(call ssh,'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE)')
|
$(call ssh,'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE)')
|
||||||
$(call cp,results/$(BASENAME).squashfs,/media/startos/images/$(SQFS_SUM).rootfs)
|
$(call ssh,'/usr/lib/startos/scripts/prune-boot')
|
||||||
$(call ssh,'sudo ln -rsf /media/startos/images/$(SQFS_SUM).rootfs /media/startos/config/current.rootfs')
|
$(call cp,results/$(BASENAME).squashfs,/media/startos/images/next.rootfs)
|
||||||
$(call ssh,'sudo reboot')
|
$(call ssh,'sudo CHECKSUM=$(SQFS_SUM) /usr/lib/startos/scripts/upgrade /media/startos/images/next.rootfs')
|
||||||
|
|
||||||
emulate-reflash: $(ALL_TARGETS)
|
emulate-reflash: $(STARTOS_TARGETS)
|
||||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||||
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
|
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
|
||||||
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM)
|
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM)
|
||||||
@@ -220,67 +262,65 @@ emulate-reflash: $(ALL_TARGETS)
|
|||||||
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y $(shell cat ./build/lib/depends)"')
|
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y $(shell cat ./build/lib/depends)"')
|
||||||
|
|
||||||
upload-ota: results/$(BASENAME).squashfs
|
upload-ota: results/$(BASENAME).squashfs
|
||||||
TARGET=$(TARGET) KEY=$(KEY) ./upload-ota.sh
|
TARGET=$(TARGET) KEY=$(KEY) ./build/upload-ota.sh
|
||||||
|
|
||||||
container-runtime/debian.$(ARCH).squashfs:
|
container-runtime/debian.$(ARCH).squashfs: ./container-runtime/download-base-image.sh
|
||||||
ARCH=$(ARCH) ./container-runtime/download-base-image.sh
|
ARCH=$(ARCH) ./container-runtime/download-base-image.sh
|
||||||
|
|
||||||
container-runtime/node_modules/.package-lock.json: container-runtime/package.json container-runtime/package-lock.json sdk/dist/package.json
|
container-runtime/package-lock.json: sdk/dist/package.json
|
||||||
|
npm --prefix container-runtime i
|
||||||
|
touch container-runtime/package-lock.json
|
||||||
|
|
||||||
|
container-runtime/node_modules/.package-lock.json: container-runtime/package-lock.json
|
||||||
npm --prefix container-runtime ci
|
npm --prefix container-runtime ci
|
||||||
touch container-runtime/node_modules/.package-lock.json
|
touch container-runtime/node_modules/.package-lock.json
|
||||||
|
|
||||||
sdk/base/lib/osBindings/index.ts: $(shell if [ "$(REBUILD_TYPES)" -ne 0 ]; then echo core/startos/bindings/index.ts; fi)
|
ts-bindings: core/bindings/index.ts
|
||||||
mkdir -p sdk/base/lib/osBindings
|
mkdir -p sdk/base/lib/osBindings
|
||||||
rsync -ac --delete core/startos/bindings/ sdk/base/lib/osBindings/
|
rsync -ac --delete core/bindings/ sdk/base/lib/osBindings/
|
||||||
touch sdk/base/lib/osBindings/index.ts
|
|
||||||
|
|
||||||
core/startos/bindings/index.ts: $(shell git ls-files core) $(ENVIRONMENT_FILE)
|
core/bindings/index.ts: $(call ls-files, core) $(ENVIRONMENT_FILE)
|
||||||
rm -rf core/startos/bindings
|
rm -rf core/bindings
|
||||||
./core/build-ts.sh
|
./core/build/build-ts.sh
|
||||||
ls core/startos/bindings/*.ts | sed 's/core\/startos\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' | grep -v '"./index"' | tee core/startos/bindings/index.ts
|
ls core/bindings/*.ts | sed 's/core\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' | grep -v '"./index"' | tee core/bindings/index.ts
|
||||||
npm --prefix sdk exec -- prettier --config ./sdk/base/package.json -w ./core/startos/bindings/*.ts
|
npm --prefix sdk/base exec -- prettier --config=./sdk/base/package.json -w './core/bindings/**/*.ts'
|
||||||
touch core/startos/bindings/index.ts
|
touch core/bindings/index.ts
|
||||||
|
|
||||||
sdk/dist/package.json sdk/baseDist/package.json: $(shell git ls-files sdk) sdk/base/lib/osBindings/index.ts
|
sdk/dist/package.json sdk/baseDist/package.json: $(call ls-files, sdk) sdk/base/lib/osBindings/index.ts
|
||||||
(cd sdk && make bundle)
|
(cd sdk && make bundle)
|
||||||
touch sdk/dist/package.json
|
touch sdk/dist/package.json
|
||||||
touch sdk/baseDist/package.json
|
touch sdk/baseDist/package.json
|
||||||
|
|
||||||
# TODO: make container-runtime its own makefile?
|
# TODO: make container-runtime its own makefile?
|
||||||
container-runtime/dist/index.js: container-runtime/node_modules/.package-lock.json $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json
|
container-runtime/dist/index.js: container-runtime/node_modules/.package-lock.json $(call ls-files, container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json
|
||||||
npm --prefix container-runtime run build
|
npm --prefix container-runtime run build
|
||||||
|
|
||||||
container-runtime/dist/node_modules/.package-lock.json container-runtime/dist/package.json container-runtime/dist/package-lock.json: container-runtime/package.json container-runtime/package-lock.json sdk/dist/package.json container-runtime/install-dist-deps.sh
|
container-runtime/dist/node_modules/.package-lock.json container-runtime/dist/package.json container-runtime/dist/package-lock.json: container-runtime/package.json container-runtime/package-lock.json sdk/dist/package.json container-runtime/install-dist-deps.sh
|
||||||
./container-runtime/install-dist-deps.sh
|
./container-runtime/install-dist-deps.sh
|
||||||
touch container-runtime/dist/node_modules/.package-lock.json
|
touch container-runtime/dist/node_modules/.package-lock.json
|
||||||
|
|
||||||
container-runtime/rootfs.$(ARCH).squashfs: container-runtime/debian.$(ARCH).squashfs container-runtime/container-runtime.service container-runtime/update-image.sh container-runtime/deb-install.sh container-runtime/dist/index.js container-runtime/dist/node_modules/.package-lock.json core/target/$(ARCH)-unknown-linux-musl/release/containerbox | sudo
|
container-runtime/rootfs.$(ARCH).squashfs: container-runtime/debian.$(ARCH).squashfs container-runtime/container-runtime.service container-runtime/update-image.sh container-runtime/update-image-local.sh container-runtime/deb-install.sh container-runtime/dist/index.js container-runtime/dist/node_modules/.package-lock.json core/target/$(RUST_ARCH)-unknown-linux-musl/release/start-container
|
||||||
ARCH=$(ARCH) ./container-runtime/update-image.sh
|
ARCH=$(ARCH) ./container-runtime/update-image-local.sh
|
||||||
|
|
||||||
build/lib/depends build/lib/conflicts: build/dpkg-deps/*
|
build/lib/depends build/lib/conflicts: $(ENVIRONMENT_FILE) $(PLATFORM_FILE) $(shell ls build/dpkg-deps/*)
|
||||||
build/dpkg-deps/generate.sh
|
PLATFORM=$(PLATFORM) ARCH=$(ARCH) build/dpkg-deps/generate.sh
|
||||||
|
|
||||||
$(FIRMWARE_ROMS): build/lib/firmware.json download-firmware.sh $(PLATFORM_FILE)
|
$(FIRMWARE_ROMS): build/lib/firmware.json ./build/download-firmware.sh $(PLATFORM_FILE)
|
||||||
./download-firmware.sh $(PLATFORM)
|
./build/download-firmware.sh $(PLATFORM)
|
||||||
|
|
||||||
system-images/compat/docker-images/$(ARCH).tar: $(COMPAT_SRC)
|
core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox: $(CORE_SRC) $(COMPRESSED_WEB_UIS) web/patchdb-ui-seed.json $(ENVIRONMENT_FILE)
|
||||||
cd system-images/compat && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar
|
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build/build-startbox.sh
|
||||||
|
touch core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox
|
||||||
|
|
||||||
system-images/utils/docker-images/$(ARCH).tar: $(UTILS_SRC)
|
core/target/$(RUST_ARCH)-unknown-linux-musl/release/start-container: $(CORE_SRC) $(ENVIRONMENT_FILE)
|
||||||
cd system-images/utils && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar
|
ARCH=$(ARCH) ./core/build/build-start-container.sh
|
||||||
|
touch core/target/$(RUST_ARCH)-unknown-linux-musl/release/start-container
|
||||||
|
|
||||||
system-images/binfmt/docker-images/$(ARCH).tar: $(BINFMT_SRC)
|
web/package-lock.json: web/package.json sdk/baseDist/package.json
|
||||||
cd system-images/binfmt && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar
|
npm --prefix web i
|
||||||
|
touch web/package-lock.json
|
||||||
|
|
||||||
core/target/$(ARCH)-unknown-linux-musl/release/startbox: $(CORE_SRC) $(COMPRESSED_WEB_UIS) web/patchdb-ui-seed.json $(ENVIRONMENT_FILE)
|
web/node_modules/.package-lock.json: web/package-lock.json
|
||||||
ARCH=$(ARCH) ./core/build-startbox.sh
|
|
||||||
touch core/target/$(ARCH)-unknown-linux-musl/release/startbox
|
|
||||||
|
|
||||||
core/target/$(ARCH)-unknown-linux-musl/release/containerbox: $(CORE_SRC) $(ENVIRONMENT_FILE)
|
|
||||||
ARCH=$(ARCH) ./core/build-containerbox.sh
|
|
||||||
touch core/target/$(ARCH)-unknown-linux-musl/release/containerbox
|
|
||||||
|
|
||||||
web/node_modules/.package-lock.json: web/package.json sdk/baseDist/package.json
|
|
||||||
npm --prefix web ci
|
npm --prefix web ci
|
||||||
touch web/node_modules/.package-lock.json
|
touch web/node_modules/.package-lock.json
|
||||||
|
|
||||||
@@ -289,23 +329,27 @@ web/.angular/.updated: patch-db/client/dist/index.js sdk/baseDist/package.json w
|
|||||||
mkdir -p web/.angular
|
mkdir -p web/.angular
|
||||||
touch web/.angular/.updated
|
touch web/.angular/.updated
|
||||||
|
|
||||||
web/dist/raw/ui/index.html: $(WEB_UI_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
|
web/.i18n-checked: $(WEB_SHARED_SRC) $(WEB_UI_SRC) $(WEB_SETUP_WIZARD_SRC) $(WEB_START_TUNNEL_SRC)
|
||||||
|
npm --prefix web run check:i18n
|
||||||
|
touch web/.i18n-checked
|
||||||
|
|
||||||
|
web/dist/raw/ui/index.html: $(WEB_UI_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/.i18n-checked
|
||||||
npm --prefix web run build:ui
|
npm --prefix web run build:ui
|
||||||
touch web/dist/raw/ui/index.html
|
touch web/dist/raw/ui/index.html
|
||||||
|
|
||||||
web/dist/raw/setup-wizard/index.html: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
|
web/dist/raw/setup-wizard/index.html: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/.i18n-checked
|
||||||
npm --prefix web run build:setup
|
npm --prefix web run build:setup
|
||||||
touch web/dist/raw/setup-wizard/index.html
|
touch web/dist/raw/setup-wizard/index.html
|
||||||
|
|
||||||
web/dist/raw/install-wizard/index.html: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
|
web/dist/raw/start-tunnel/index.html: $(WEB_START_TUNNEL_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/.i18n-checked
|
||||||
npm --prefix web run build:install-wiz
|
npm --prefix web run build:tunnel
|
||||||
touch web/dist/raw/install-wizard/index.html
|
touch web/dist/raw/start-tunnel/index.html
|
||||||
|
|
||||||
$(COMPRESSED_WEB_UIS): $(WEB_UIS) $(ENVIRONMENT_FILE)
|
web/dist/static/%/index.html: web/dist/raw/%/index.html
|
||||||
./compress-uis.sh
|
./web/compress-uis.sh $*
|
||||||
|
|
||||||
web/config.json: $(GIT_HASH_FILE) web/config-sample.json
|
web/config.json: $(GIT_HASH_FILE) $(ENVIRONMENT_FILE) web/config-sample.json web/update-config.sh
|
||||||
jq '.useMocks = false' web/config-sample.json | jq '.gitHash = "$(shell cat GIT_HASH.txt)"' > web/config.json
|
./web/update-config.sh
|
||||||
|
|
||||||
patch-db/client/node_modules/.package-lock.json: patch-db/client/package.json
|
patch-db/client/node_modules/.package-lock.json: patch-db/client/package.json
|
||||||
npm --prefix patch-db/client ci
|
npm --prefix patch-db/client ci
|
||||||
@@ -326,11 +370,17 @@ uis: $(WEB_UIS)
|
|||||||
# this is a convenience step to build the UI
|
# this is a convenience step to build the UI
|
||||||
ui: web/dist/raw/ui
|
ui: web/dist/raw/ui
|
||||||
|
|
||||||
cargo-deps/aarch64-unknown-linux-musl/release/pi-beep:
|
target/aarch64-unknown-linux-musl/release/pi-beep: ./build/build-cargo-dep.sh
|
||||||
ARCH=aarch64 ./build-cargo-dep.sh pi-beep
|
ARCH=aarch64 ./build/build-cargo-dep.sh pi-beep
|
||||||
|
|
||||||
cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console:
|
target/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console: ./build/build-cargo-dep.sh
|
||||||
ARCH=$(ARCH) PREINSTALL="apk add musl-dev pkgconfig" ./build-cargo-dep.sh tokio-console
|
ARCH=$(ARCH) ./build/build-cargo-dep.sh tokio-console
|
||||||
|
touch $@
|
||||||
|
|
||||||
cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs:
|
target/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs: ./build/build-cargo-dep.sh
|
||||||
ARCH=$(ARCH) PREINSTALL="apk add fuse3 fuse3-dev fuse3-static musl-dev pkgconfig" ./build-cargo-dep.sh --git https://github.com/Start9Labs/start-fs.git startos-backup-fs
|
ARCH=$(ARCH) ./build/build-cargo-dep.sh --git https://github.com/Start9Labs/start-fs.git startos-backup-fs
|
||||||
|
touch $@
|
||||||
|
|
||||||
|
target/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph: ./build/build-cargo-dep.sh
|
||||||
|
ARCH=$(ARCH) ./build/build-cargo-dep.sh flamegraph
|
||||||
|
touch $@
|
||||||
|
|||||||
91
README.md
@@ -7,79 +7,64 @@
|
|||||||
<a href="https://github.com/Start9Labs/start-os/actions/workflows/startos-iso.yaml">
|
<a href="https://github.com/Start9Labs/start-os/actions/workflows/startos-iso.yaml">
|
||||||
<img src="https://github.com/Start9Labs/start-os/actions/workflows/startos-iso.yaml/badge.svg">
|
<img src="https://github.com/Start9Labs/start-os/actions/workflows/startos-iso.yaml/badge.svg">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://heyapollo.com/product/startos">
|
<a href="https://heyapollo.com/product/startos">
|
||||||
<img alt="Static Badge" src="https://img.shields.io/badge/apollo-review%20%E2%AD%90%E2%AD%90%E2%AD%90%E2%AD%90%E2%AD%90%20-slateblue">
|
<img alt="Static Badge" src="https://img.shields.io/badge/apollo-review%20%E2%AD%90%E2%AD%90%E2%AD%90%E2%AD%90%E2%AD%90%20-slateblue">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://twitter.com/start9labs">
|
<a href="https://twitter.com/start9labs">
|
||||||
<img alt="X (formerly Twitter) Follow" src="https://img.shields.io/twitter/follow/start9labs">
|
<img alt="X (formerly Twitter) Follow" src="https://img.shields.io/twitter/follow/start9labs">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://mastodon.start9labs.com">
|
|
||||||
<img src="https://img.shields.io/mastodon/follow/000000001?domain=https%3A%2F%2Fmastodon.start9labs.com&label=Follow&style=social">
|
|
||||||
</a>
|
|
||||||
<a href="https://matrix.to/#/#community:matrix.start9labs.com">
|
|
||||||
<img alt="Static Badge" src="https://img.shields.io/badge/community-matrix-yellow?logo=matrix">
|
|
||||||
</a>
|
|
||||||
<a href="https://t.me/start9_labs">
|
|
||||||
<img alt="Static Badge" src="https://img.shields.io/badge/community-telegram-blue?logo=telegram">
|
|
||||||
</a>
|
|
||||||
<a href="https://docs.start9.com">
|
<a href="https://docs.start9.com">
|
||||||
<img alt="Static Badge" src="https://img.shields.io/badge/docs-orange?label=%F0%9F%91%A4%20support">
|
<img alt="Static Badge" src="https://img.shields.io/badge/docs-orange?label=%F0%9F%91%A4%20support">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://matrix.to/#/#community-dev:matrix.start9labs.com">
|
<a href="https://matrix.to/#/#dev-startos:matrix.start9labs.com">
|
||||||
<img alt="Static Badge" src="https://img.shields.io/badge/developer-matrix-darkcyan?logo=matrix">
|
<img alt="Static Badge" src="https://img.shields.io/badge/developer-matrix-darkcyan?logo=matrix">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://start9.com">
|
<a href="https://start9.com">
|
||||||
<img alt="Website" src="https://img.shields.io/website?up_message=online&down_message=offline&url=https%3A%2F%2Fstart9.com&logo=website&label=%F0%9F%8C%90%20website">
|
<img alt="Website" src="https://img.shields.io/website?up_message=online&down_message=offline&url=https%3A%2F%2Fstart9.com&logo=website&label=%F0%9F%8C%90%20website">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
|
||||||
<div align="center">
|
|
||||||
<h3>
|
|
||||||
Welcome to the era of Sovereign Computing
|
|
||||||
</h3>
|
|
||||||
<p>
|
|
||||||
StartOS is an open source Linux distribution optimized for running a personal server. It facilitates the discovery, installation, network configuration, service configuration, data backup, dependency management, and health monitoring of self-hosted software services.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<p align="center">
|
|
||||||
<img src="assets/StartOS.png" alt="StartOS" width="85%">
|
|
||||||
</p>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
## Running StartOS
|
## What is StartOS?
|
||||||
> [!WARNING]
|
|
||||||
> StartOS is in beta. It lacks features. It doesn't always work perfectly. Start9 servers are not plug and play. Using them properly requires some effort and patience. Please do not use StartOS or purchase a server if you are unable or unwilling to follow instructions and learn new concepts.
|
|
||||||
|
|
||||||
### 💰 Buy a Start9 server
|
StartOS is an open-source Linux distribution for running a personal server. It handles discovery, installation, network configuration, data backup, dependency management, and health monitoring of self-hosted services.
|
||||||
This is the most convenient option. Simply [buy a server](https://store.start9.com) from Start9 and plug it in.
|
|
||||||
|
|
||||||
### 👷 Build your own server
|
**Tech stack:** Rust backend (Tokio/Axum), Angular frontend, Node.js container runtime with LXC, and a custom diff-based database ([Patch-DB](https://github.com/Start9Labs/patch-db)) for reactive state synchronization.
|
||||||
This option is easier than you might imagine, and there are 4 reasons why you might prefer it:
|
|
||||||
1. You already have hardware
|
|
||||||
1. You want to save on shipping costs
|
|
||||||
1. You prefer not to divulge your physical address
|
|
||||||
1. You just like building things
|
|
||||||
|
|
||||||
To pursue this option, follow one of our [DIY guides](https://start9.com/latest/diy).
|
Services run in isolated LXC containers, packaged as [S9PKs](https://github.com/Start9Labs/start-os/blob/master/core/s9pk-structure.md) — a signed, merkle-archived format that supports partial downloads and cryptographic verification.
|
||||||
|
|
||||||
## ❤️ Contributing
|
## What can you do with it?
|
||||||
There are multiple ways to contribute: work directly on StartOS, package a service for the marketplace, or help with documentation and guides. To learn more about contributing, see [here](https://start9.com/contribute/).
|
|
||||||
|
|
||||||
To report security issues, please email our security team - security@start9.com.
|
StartOS lets you self-host services that would otherwise depend on third-party cloud providers — giving you full ownership of your data and infrastructure.
|
||||||
|
|
||||||
## 🌎 Marketplace
|
Browse available services on the [Start9 Marketplace](https://marketplace.start9.com/), including:
|
||||||
There are dozens of services available for StartOS, and new ones are being added all the time. Check out the full list of available services [here](https://marketplace.start9.com/marketplace). To read more about the Marketplace ecosystem, check out this [blog post](https://blog.start9.com/start9-marketplace-strategy/)
|
|
||||||
|
|
||||||
## 🖥️ User Interface Screenshots
|
- **Bitcoin & Lightning** — Run a full Bitcoin node, Lightning node, BTCPay Server, and other payment infrastructure
|
||||||
|
- **Communication** — Self-host Matrix, SimpleX, or other messaging platforms
|
||||||
|
- **Cloud Storage** — Run Nextcloud, Vaultwarden, and other productivity tools
|
||||||
|
|
||||||
<p align="center">
|
Services are added by the community. If a service you want isn't available, you can [package it yourself](https://github.com/Start9Labs/ai-service-packaging/).
|
||||||
<img src="assets/registry.png" alt="StartOS Marketplace" width="49%">
|
|
||||||
<img src="assets/community.png" alt="StartOS Community Registry" width="49%">
|
## Getting StartOS
|
||||||
<img src="assets/c-lightning.png" alt="StartOS NextCloud Service" width="49%">
|
|
||||||
<img src="assets/btcpay.png" alt="StartOS BTCPay Service" width="49%">
|
### Buy a Start9 server
|
||||||
<img src="assets/nextcloud.png" alt="StartOS System Settings" width="49%">
|
|
||||||
<img src="assets/system.png" alt="StartOS System Settings" width="49%">
|
The easiest path. [Buy a server](https://store.start9.com) from Start9 and plug it in.
|
||||||
<img src="assets/welcome.png" alt="StartOS System Settings" width="49%">
|
|
||||||
<img src="assets/logs.png" alt="StartOS System Settings" width="49%">
|
### Build your own
|
||||||
</p>
|
|
||||||
|
Install StartOS on your own hardware. Follow one of the [DIY guides](https://start9.com/latest/diy). Reasons to go this route:
|
||||||
|
|
||||||
|
1. You already have compatible hardware
|
||||||
|
2. You want to save on shipping costs
|
||||||
|
3. You prefer not to share your physical address
|
||||||
|
4. You enjoy building things
|
||||||
|
|
||||||
|
### Build from source
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for environment setup, build instructions, and development workflow.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
There are multiple ways to contribute: work directly on StartOS, package a service for the marketplace, or help with documentation and guides. See [CONTRIBUTING.md](CONTRIBUTING.md) or visit [start9.com/contribute](https://start9.com/contribute/).
|
||||||
|
|
||||||
|
To report security issues, email [security@start9.com](mailto:security@start9.com).
|
||||||
|
|||||||
BIN
apt/start9.gpg
Normal file
1
apt/start9.list
Normal file
@@ -0,0 +1 @@
|
|||||||
|
deb [arch=amd64,arm64,riscv64 signed-by=/usr/share/keyrings/start9.gpg] https://start9-debs.nyc3.cdn.digitaloceanspaces.com stable main
|
||||||
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 396 KiB |
|
Before Width: | Height: | Size: 402 KiB |
|
Before Width: | Height: | Size: 591 KiB |
BIN
assets/logs.png
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 319 KiB |
|
Before Width: | Height: | Size: 521 KiB |
|
Before Width: | Height: | Size: 331 KiB |
|
Before Width: | Height: | Size: 402 KiB |
@@ -1,34 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
shopt -s expand_aliases
|
|
||||||
|
|
||||||
if [ "$0" != "./build-cargo-dep.sh" ]; then
|
|
||||||
>&2 echo "Must be run from start-os directory"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
USE_TTY=
|
|
||||||
if tty -s; then
|
|
||||||
USE_TTY="-it"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$ARCH" ]; then
|
|
||||||
ARCH=$(uname -m)
|
|
||||||
fi
|
|
||||||
|
|
||||||
DOCKER_PLATFORM="linux/${ARCH}"
|
|
||||||
if [ "$ARCH" = aarch64 ] || [ "$ARCH" = arm64 ]; then
|
|
||||||
DOCKER_PLATFORM="linux/arm64"
|
|
||||||
elif [ "$ARCH" = x86_64 ]; then
|
|
||||||
DOCKER_PLATFORM="linux/amd64"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p cargo-deps
|
|
||||||
alias 'rust-musl-builder'='docker run $USE_TTY --platform=${DOCKER_PLATFORM} --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)"/cargo-deps:/home/rust/src -w /home/rust/src -P rust:alpine'
|
|
||||||
|
|
||||||
PREINSTALL=${PREINSTALL:-true}
|
|
||||||
|
|
||||||
rust-musl-builder sh -c "$PREINSTALL && cargo install $* --target-dir /home/rust/src --target=$ARCH-unknown-linux-musl"
|
|
||||||
sudo chown -R $USER cargo-deps
|
|
||||||
sudo chown -R $USER ~/.cargo
|
|
||||||
138
build/apt/publish-deb.sh
Executable file
@@ -0,0 +1,138 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Publish .deb files to an S3-hosted apt repository.
|
||||||
|
#
|
||||||
|
# Usage: publish-deb.sh <deb-file-or-directory> [<deb-file-or-directory> ...]
|
||||||
|
#
|
||||||
|
# Environment variables:
|
||||||
|
# GPG_PRIVATE_KEY - Armored GPG private key (imported if set)
|
||||||
|
# GPG_KEY_ID - GPG key ID for signing
|
||||||
|
# S3_ACCESS_KEY - S3 access key
|
||||||
|
# S3_SECRET_KEY - S3 secret key
|
||||||
|
# S3_ENDPOINT - S3 endpoint (default: https://nyc3.digitaloceanspaces.com)
|
||||||
|
# S3_BUCKET - S3 bucket name (default: start9-debs)
|
||||||
|
# SUITE - Apt suite name (default: stable)
|
||||||
|
# COMPONENT - Apt component name (default: main)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
echo "Usage: $0 <deb-file-or-directory> [...]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BUCKET="${S3_BUCKET:-start9-debs}"
|
||||||
|
ENDPOINT="${S3_ENDPOINT:-https://nyc3.digitaloceanspaces.com}"
|
||||||
|
SUITE="${SUITE:-stable}"
|
||||||
|
COMPONENT="${COMPONENT:-main}"
|
||||||
|
REPO_DIR="$(mktemp -d)"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$REPO_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Import GPG key if provided
|
||||||
|
if [ -n "$GPG_PRIVATE_KEY" ]; then
|
||||||
|
echo "$GPG_PRIVATE_KEY" | gpg --batch --import 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configure s3cmd
|
||||||
|
if [ -n "$S3_ACCESS_KEY" ] && [ -n "$S3_SECRET_KEY" ]; then
|
||||||
|
S3CMD_CONFIG="$(mktemp)"
|
||||||
|
cat > "$S3CMD_CONFIG" <<EOF
|
||||||
|
[default]
|
||||||
|
access_key = ${S3_ACCESS_KEY}
|
||||||
|
secret_key = ${S3_SECRET_KEY}
|
||||||
|
host_base = $(echo "$ENDPOINT" | sed 's|https://||')
|
||||||
|
host_bucket = %(bucket)s.$(echo "$ENDPOINT" | sed 's|https://||')
|
||||||
|
use_https = True
|
||||||
|
EOF
|
||||||
|
s3() {
|
||||||
|
s3cmd -c "$S3CMD_CONFIG" "$@"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
# Fall back to default ~/.s3cfg
|
||||||
|
S3CMD_CONFIG=""
|
||||||
|
s3() {
|
||||||
|
s3cmd "$@"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sync existing repo from S3
|
||||||
|
echo "Syncing existing repo from s3://${BUCKET}/ ..."
|
||||||
|
s3 sync --no-mime-magic "s3://${BUCKET}/" "$REPO_DIR/" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Collect all .deb files from arguments
|
||||||
|
DEB_FILES=()
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ -d "$arg" ]; then
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
DEB_FILES+=("$f")
|
||||||
|
done < <(find "$arg" -name '*.deb' -print0)
|
||||||
|
elif [ -f "$arg" ]; then
|
||||||
|
DEB_FILES+=("$arg")
|
||||||
|
else
|
||||||
|
echo "Warning: $arg is not a file or directory, skipping" >&2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#DEB_FILES[@]} -eq 0 ]; then
|
||||||
|
echo "No .deb files found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy each deb to the pool, renaming to standard format
|
||||||
|
for deb in "${DEB_FILES[@]}"; do
|
||||||
|
PKG_NAME="$(dpkg-deb --field "$deb" Package)"
|
||||||
|
POOL_DIR="$REPO_DIR/pool/${COMPONENT}/${PKG_NAME:0:1}/${PKG_NAME}"
|
||||||
|
mkdir -p "$POOL_DIR"
|
||||||
|
cp "$deb" "$POOL_DIR/"
|
||||||
|
dpkg-name -o "$POOL_DIR/$(basename "$deb")" 2>/dev/null || true
|
||||||
|
echo "Added: $(basename "$deb") -> pool/${COMPONENT}/${PKG_NAME:0:1}/${PKG_NAME}/"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Generate Packages indices for each architecture
|
||||||
|
for arch in amd64 arm64 riscv64; do
|
||||||
|
BINARY_DIR="$REPO_DIR/dists/${SUITE}/${COMPONENT}/binary-${arch}"
|
||||||
|
mkdir -p "$BINARY_DIR"
|
||||||
|
(
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
dpkg-scanpackages --arch "$arch" pool/ > "$BINARY_DIR/Packages"
|
||||||
|
gzip -k -f "$BINARY_DIR/Packages"
|
||||||
|
)
|
||||||
|
echo "Generated Packages index for ${arch}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Generate Release file
|
||||||
|
(
|
||||||
|
cd "$REPO_DIR/dists/${SUITE}"
|
||||||
|
apt-ftparchive release \
|
||||||
|
-o "APT::FTPArchive::Release::Origin=Start9" \
|
||||||
|
-o "APT::FTPArchive::Release::Label=Start9" \
|
||||||
|
-o "APT::FTPArchive::Release::Suite=${SUITE}" \
|
||||||
|
-o "APT::FTPArchive::Release::Codename=${SUITE}" \
|
||||||
|
-o "APT::FTPArchive::Release::Architectures=amd64 arm64 riscv64" \
|
||||||
|
-o "APT::FTPArchive::Release::Components=${COMPONENT}" \
|
||||||
|
. > Release
|
||||||
|
)
|
||||||
|
echo "Generated Release file"
|
||||||
|
|
||||||
|
# Sign if GPG key is available
|
||||||
|
if [ -n "$GPG_KEY_ID" ]; then
|
||||||
|
(
|
||||||
|
cd "$REPO_DIR/dists/${SUITE}"
|
||||||
|
gpg --default-key "$GPG_KEY_ID" --batch --yes --detach-sign -o Release.gpg Release
|
||||||
|
gpg --default-key "$GPG_KEY_ID" --batch --yes --clearsign -o InRelease Release
|
||||||
|
)
|
||||||
|
echo "Signed Release file with key ${GPG_KEY_ID}"
|
||||||
|
else
|
||||||
|
echo "Warning: GPG_KEY_ID not set, Release file is unsigned" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upload to S3
|
||||||
|
echo "Uploading to s3://${BUCKET}/ ..."
|
||||||
|
s3 sync --acl-public --no-mime-magic "$REPO_DIR/" "s3://${BUCKET}/"
|
||||||
|
|
||||||
|
[ -n "$S3CMD_CONFIG" ] && rm -f "$S3CMD_CONFIG"
|
||||||
|
echo "Done."
|
||||||
26
build/build-cargo-dep.sh
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||||
|
|
||||||
|
set -e
|
||||||
|
shopt -s expand_aliases
|
||||||
|
|
||||||
|
if [ -z "$ARCH" ]; then
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUST_ARCH="$ARCH"
|
||||||
|
if [ "$ARCH" = "riscv64" ]; then
|
||||||
|
RUST_ARCH="riscv64gc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p target
|
||||||
|
|
||||||
|
source core/build/builder-alias.sh
|
||||||
|
|
||||||
|
RUSTFLAGS="-C target-feature=+crt-static"
|
||||||
|
|
||||||
|
rust-zig-builder cargo-zigbuild install $* --target-dir /workdir/target/ --target=$RUST_ARCH-unknown-linux-musl
|
||||||
|
if [ "$(ls -nd "target/$RUST_ARCH-unknown-linux-musl/release/${!#}" | awk '{ print $3 }')" != "$UID" ]; then
|
||||||
|
rust-zig-builder sh -c "chown -R $UID:$UID target && chown -R $UID:$UID /usr/local/cargo"
|
||||||
|
fi
|
||||||
@@ -11,13 +11,13 @@ if [ -z "$PLATFORM" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -rf ./firmware/$PLATFORM
|
rm -rf ./lib/firmware/$PLATFORM
|
||||||
mkdir -p ./firmware/$PLATFORM
|
mkdir -p ./lib/firmware/$PLATFORM
|
||||||
|
|
||||||
cd ./firmware/$PLATFORM
|
cd ./lib/firmware/$PLATFORM
|
||||||
|
|
||||||
firmwares=()
|
firmwares=()
|
||||||
while IFS= read -r line; do firmwares+=("$line"); done < <(jq -c ".[] | select(.platform[] | contains(\"$PLATFORM\"))" ../../build/lib/firmware.json)
|
while IFS= read -r line; do firmwares+=("$line"); done < <(jq -c ".[] | select(.platform[] | contains(\"$PLATFORM\"))" ../../firmware.json)
|
||||||
for firmware in "${firmwares[@]}"; do
|
for firmware in "${firmwares[@]}"; do
|
||||||
if [ -n "$firmware" ]; then
|
if [ -n "$firmware" ]; then
|
||||||
id=$(echo "$firmware" | jq --raw-output '.id')
|
id=$(echo "$firmware" | jq --raw-output '.id')
|
||||||
@@ -3,22 +3,25 @@ avahi-utils
|
|||||||
b3sum
|
b3sum
|
||||||
bash-completion
|
bash-completion
|
||||||
beep
|
beep
|
||||||
|
binfmt-support
|
||||||
bmon
|
bmon
|
||||||
btrfs-progs
|
btrfs-progs
|
||||||
ca-certificates
|
ca-certificates
|
||||||
cifs-utils
|
cifs-utils
|
||||||
|
conntrack
|
||||||
cryptsetup
|
cryptsetup
|
||||||
curl
|
curl
|
||||||
dnsutils
|
|
||||||
dmidecode
|
dmidecode
|
||||||
dnsutils
|
dnsutils
|
||||||
dosfstools
|
dosfstools
|
||||||
e2fsprogs
|
e2fsprogs
|
||||||
ecryptfs-utils
|
ecryptfs-utils
|
||||||
|
equivs
|
||||||
exfatprogs
|
exfatprogs
|
||||||
flashrom
|
flashrom
|
||||||
fuse3
|
fuse3
|
||||||
grub-common
|
grub-common
|
||||||
|
grub-efi
|
||||||
htop
|
htop
|
||||||
httpdirfs
|
httpdirfs
|
||||||
iotop
|
iotop
|
||||||
@@ -36,13 +39,15 @@ man-db
|
|||||||
ncdu
|
ncdu
|
||||||
net-tools
|
net-tools
|
||||||
network-manager
|
network-manager
|
||||||
|
nfs-common
|
||||||
nvme-cli
|
nvme-cli
|
||||||
nyx
|
nyx
|
||||||
openssh-server
|
openssh-server
|
||||||
podman
|
podman
|
||||||
postgresql
|
|
||||||
psmisc
|
psmisc
|
||||||
qemu-guest-agent
|
qemu-guest-agent
|
||||||
|
qemu-user-static
|
||||||
|
rfkill
|
||||||
rsync
|
rsync
|
||||||
samba-common-bin
|
samba-common-bin
|
||||||
smartmontools
|
smartmontools
|
||||||
@@ -50,6 +55,7 @@ socat
|
|||||||
sqlite3
|
sqlite3
|
||||||
squashfs-tools
|
squashfs-tools
|
||||||
squashfs-tools-ng
|
squashfs-tools-ng
|
||||||
|
ssl-cert
|
||||||
sudo
|
sudo
|
||||||
systemd
|
systemd
|
||||||
systemd-resolved
|
systemd-resolved
|
||||||
|
|||||||
1
build/dpkg-deps/dev.depends
Normal file
@@ -0,0 +1 @@
|
|||||||
|
+ nmap
|
||||||
@@ -5,11 +5,18 @@ set -e
|
|||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
IFS="-" read -ra FEATURES <<< "$ENVIRONMENT"
|
IFS="-" read -ra FEATURES <<< "$ENVIRONMENT"
|
||||||
|
FEATURES+=("${ARCH}")
|
||||||
|
if [ "$ARCH" != "$PLATFORM" ]; then
|
||||||
|
FEATURES+=("${PLATFORM}")
|
||||||
|
fi
|
||||||
|
if [[ "$PLATFORM" =~ -nonfree$ ]]; then
|
||||||
|
FEATURES+=("nonfree")
|
||||||
|
fi
|
||||||
|
|
||||||
feature_file_checker='
|
feature_file_checker='
|
||||||
/^#/ { next }
|
/^#/ { next }
|
||||||
/^\+ [a-z0-9]+$/ { next }
|
/^\+ [a-z0-9.-]+$/ { next }
|
||||||
/^- [a-z0-9]+$/ { next }
|
/^- [a-z0-9.-]+$/ { next }
|
||||||
{ exit 1 }
|
{ exit 1 }
|
||||||
'
|
'
|
||||||
|
|
||||||
@@ -30,8 +37,8 @@ for type in conflicts depends; do
|
|||||||
for feature in ${FEATURES[@]}; do
|
for feature in ${FEATURES[@]}; do
|
||||||
file="$feature.$type"
|
file="$feature.$type"
|
||||||
if [ -f $file ]; then
|
if [ -f $file ]; then
|
||||||
if grep "^- $pkg$" $file; then
|
if grep "^- $pkg$" $file > /dev/null; then
|
||||||
SKIP=1
|
SKIP=yes
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
10
build/dpkg-deps/nonfree.depends
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
+ firmware-amd-graphics
|
||||||
|
+ firmware-atheros
|
||||||
|
+ firmware-brcm80211
|
||||||
|
+ firmware-iwlwifi
|
||||||
|
+ firmware-libertas
|
||||||
|
+ firmware-misc-nonfree
|
||||||
|
+ firmware-realtek
|
||||||
|
+ nvidia-container-toolkit
|
||||||
|
# + nvidia-driver
|
||||||
|
# + nvidia-kernel-dkms
|
||||||
10
build/dpkg-deps/raspberrypi.depends
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
- grub-efi
|
||||||
|
+ parted
|
||||||
|
+ raspberrypi-net-mods
|
||||||
|
+ raspberrypi-sys-mods
|
||||||
|
+ raspi-config
|
||||||
|
+ raspi-firmware
|
||||||
|
+ raspi-utils
|
||||||
|
+ rpi-eeprom
|
||||||
|
+ rpi-update
|
||||||
|
+ rpi.gpio-common
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
+ gdb
|
+ gdb
|
||||||
+ heaptrack
|
+ heaptrack
|
||||||
|
+ linux-perf
|
||||||
1
build/dpkg-deps/x86_64.depends
Normal file
@@ -0,0 +1 @@
|
|||||||
|
+ grub-pc-bin
|
||||||
4
basename.sh → build/env/basename.sh
vendored
@@ -1,5 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
PROJECT=${PROJECT:-"startos"}
|
||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
PLATFORM="$(if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi)"
|
PLATFORM="$(if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi)"
|
||||||
@@ -16,4 +18,4 @@ if [ -n "$STARTOS_ENV" ]; then
|
|||||||
VERSION_FULL="$VERSION_FULL~${STARTOS_ENV}"
|
VERSION_FULL="$VERSION_FULL~${STARTOS_ENV}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -n "startos-${VERSION_FULL}_${PLATFORM}"
|
echo -n "${PROJECT}-${VERSION_FULL}_${PLATFORM}"
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
if ! [ -f ./ENVIRONMENT.txt ] || [ "$(cat ./ENVIRONMENT.txt)" != "$ENVIRONMENT" ]; then
|
if ! [ -f ./ENVIRONMENT.txt ] || [ "$(cat ./ENVIRONMENT.txt)" != "$ENVIRONMENT" ]; then
|
||||||
>&2 echo "Updating ENVIRONMENT.txt to \"$ENVIRONMENT\""
|
>&2 echo "Updating ENVIRONMENT.txt to \"$ENVIRONMENT\""
|
||||||
echo -n "$ENVIRONMENT" > ./ENVIRONMENT.txt
|
echo -n "$ENVIRONMENT" > ./ENVIRONMENT.txt
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -n ./ENVIRONMENT.txt
|
echo -n ./build/env/ENVIRONMENT.txt
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
if [ "$GIT_BRANCH_AS_HASH" != 1 ]; then
|
if [ "$GIT_BRANCH_AS_HASH" != 1 ]; then
|
||||||
GIT_HASH="$(git rev-parse HEAD)$(if ! git diff-index --quiet HEAD --; then echo '-modified'; fi)"
|
GIT_HASH="$(git rev-parse HEAD)$(if ! git diff-index --quiet HEAD --; then echo '-modified'; fi)"
|
||||||
else
|
else
|
||||||
@@ -7,7 +9,8 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if ! [ -f ./GIT_HASH.txt ] || [ "$(cat ./GIT_HASH.txt)" != "$GIT_HASH" ]; then
|
if ! [ -f ./GIT_HASH.txt ] || [ "$(cat ./GIT_HASH.txt)" != "$GIT_HASH" ]; then
|
||||||
|
>&2 echo Git hash changed from "$([ -f ./GIT_HASH.txt ] && cat ./GIT_HASH.txt)" to "$GIT_HASH"
|
||||||
echo -n "$GIT_HASH" > ./GIT_HASH.txt
|
echo -n "$GIT_HASH" > ./GIT_HASH.txt
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -n ./GIT_HASH.txt
|
echo -n ./build/env/GIT_HASH.txt
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
if ! [ -f ./PLATFORM.txt ] || [ "$(cat ./PLATFORM.txt)" != "$PLATFORM" ] && [ -n "$PLATFORM" ]; then
|
if ! [ -f ./PLATFORM.txt ] || [ "$(cat ./PLATFORM.txt)" != "$PLATFORM" ] && [ -n "$PLATFORM" ]; then
|
||||||
>&2 echo "Updating PLATFORM.txt to \"$PLATFORM\""
|
>&2 echo "Updating PLATFORM.txt to \"$PLATFORM\""
|
||||||
echo -n "$PLATFORM" > ./PLATFORM.txt
|
echo -n "$PLATFORM" > ./PLATFORM.txt
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -n ./PLATFORM.txt
|
echo -n ./build/env/PLATFORM.txt
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
FE_VERSION="$(cat web/package.json | grep '"version"' | sed 's/[ \t]*"version":[ \t]*"\([^"]*\)",/\1/')"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
|
FE_VERSION="$(cat ../../web/package.json | grep '"version"' | sed 's/[ \t]*"version":[ \t]*"\([^"]*\)",/\1/')"
|
||||||
|
|
||||||
# TODO: Validate other version sources - backend/Cargo.toml, backend/src/version/mod.rs
|
# TODO: Validate other version sources - backend/Cargo.toml, backend/src/version/mod.rs
|
||||||
|
|
||||||
@@ -10,4 +12,4 @@ if ! [ -f ./VERSION.txt ] || [ "$(cat ./VERSION.txt)" != "$VERSION" ]; then
|
|||||||
echo -n "$VERSION" > ./VERSION.txt
|
echo -n "$VERSION" > ./VERSION.txt
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -n ./VERSION.txt
|
echo -n ./build/env/VERSION.txt
|
||||||
35
build/image-recipe/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
ARG SUITE=trixie
|
||||||
|
|
||||||
|
FROM debian:${SUITE}
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -yq \
|
||||||
|
live-build \
|
||||||
|
procps \
|
||||||
|
binfmt-support \
|
||||||
|
qemu-utils \
|
||||||
|
qemu-user-static \
|
||||||
|
xorriso \
|
||||||
|
isolinux \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
gpg \
|
||||||
|
git \
|
||||||
|
fdisk \
|
||||||
|
dosfstools \
|
||||||
|
e2fsprogs \
|
||||||
|
squashfs-tools \
|
||||||
|
rsync \
|
||||||
|
b3sum \
|
||||||
|
dpkg-dev
|
||||||
|
|
||||||
|
|
||||||
|
COPY binary_grub-efi.patch /root/binary_grub-efi.patch
|
||||||
|
RUN patch /usr/lib/live/build/binary_grub-efi < /root/binary_grub-efi.patch && rm /root/binary_grub-efi.patch
|
||||||
|
|
||||||
|
RUN echo 'retry_connrefused = on' > /etc/wgetrc && \
|
||||||
|
echo 'tries = 100' >> /etc/wgetrc
|
||||||
|
|
||||||
|
WORKDIR /root
|
||||||
47
build/image-recipe/binary_grub-efi.patch
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
--- /usr/lib/live/build/binary_grub-efi 2024-05-25 05:22:52.000000000 -0600
|
||||||
|
+++ binary_grub-efi 2025-10-16 13:04:32.338740922 -0600
|
||||||
|
@@ -54,6 +54,8 @@
|
||||||
|
armhf)
|
||||||
|
Check_package chroot /usr/lib/grub/arm-efi/configfile.mod grub-efi-arm-bin
|
||||||
|
;;
|
||||||
|
+ riscv64)
|
||||||
|
+ Check_package chroot /usr/lib/grub/riscv64-efi/configfile.mod grub-efi-riscv64-bin
|
||||||
|
esac
|
||||||
|
Check_package chroot /usr/bin/grub-mkimage grub-common
|
||||||
|
Check_package chroot /usr/bin/mcopy mtools
|
||||||
|
@@ -136,7 +138,7 @@
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Cleanup files that we generate
|
||||||
|
-rm -rf binary/boot/efi.img binary/boot/grub/i386-efi/ binary/boot/grub/x86_64-efi binary/boot/grub/arm64-efi binary/boot/grub/arm-efi
|
||||||
|
+rm -rf binary/boot/efi.img binary/boot/grub/i386-efi/ binary/boot/grub/x86_64-efi binary/boot/grub/arm64-efi binary/boot/grub/arm-efi binary/boot/grub/riscv64-efi
|
||||||
|
|
||||||
|
# This is workaround till both efi-image and grub-cpmodules are put into a binary package
|
||||||
|
case "${LB_BUILD_WITH_CHROOT}" in
|
||||||
|
@@ -243,6 +245,10 @@
|
||||||
|
gen_efi_boot_img "arm-efi" "arm" "debian-live/arm"
|
||||||
|
PATH="\${PRE_EFI_IMAGE_PATH}"
|
||||||
|
;;
|
||||||
|
+ riscv64)
|
||||||
|
+ gen_efi_boot_img "riscv64-efi" "riscv64" "debian-live/riscv64"
|
||||||
|
+ PATH="\${PRE_EFI_IMAGE_PATH}"
|
||||||
|
+ ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
|
||||||
|
@@ -324,6 +330,7 @@
|
||||||
|
rm -f chroot/grub-efi-temp/bootnetx64.efi
|
||||||
|
rm -f chroot/grub-efi-temp/bootnetaa64.efi
|
||||||
|
rm -f chroot/grub-efi-temp/bootnetarm.efi
|
||||||
|
+rm -f chroot/grub-efi-temp/bootnetriscv64.efi
|
||||||
|
|
||||||
|
mkdir -p binary
|
||||||
|
cp -a chroot/grub-efi-temp/* binary/
|
||||||
|
@@ -331,6 +338,7 @@
|
||||||
|
rm -rf chroot/grub-efi-temp-i386-efi
|
||||||
|
rm -rf chroot/grub-efi-temp-arm64-efi
|
||||||
|
rm -rf chroot/grub-efi-temp-arm-efi
|
||||||
|
+rm -rf chroot/grub-efi-temp-riscv64-efi
|
||||||
|
rm -rf chroot/grub-efi-temp-cfg
|
||||||
|
rm -rf chroot/grub-efi-temp
|
||||||
|
|
||||||
449
build/image-recipe/build.sh
Executable file
@@ -0,0 +1,449 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
MAX_IMG_LEN=$((4 * 1024 * 1024 * 1024)) # 4GB
|
||||||
|
|
||||||
|
echo "==== StartOS Image Build ===="
|
||||||
|
|
||||||
|
echo "Building for architecture: $IB_TARGET_ARCH"
|
||||||
|
|
||||||
|
SOURCE_DIR="$(realpath $(dirname "${BASH_SOURCE[0]}"))"
|
||||||
|
|
||||||
|
base_dir="$(pwd -P)"
|
||||||
|
prep_results_dir="$base_dir/images-prep"
|
||||||
|
RESULTS_DIR="$base_dir/results"
|
||||||
|
echo "Saving results in: $RESULTS_DIR"
|
||||||
|
|
||||||
|
DEB_PATH="$base_dir/$1"
|
||||||
|
|
||||||
|
VERSION="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/VERSION.txt)"
|
||||||
|
GIT_HASH="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/GIT_HASH.txt)"
|
||||||
|
if [[ "$GIT_HASH" =~ ^@ ]]; then
|
||||||
|
GIT_HASH="unknown"
|
||||||
|
else
|
||||||
|
GIT_HASH="$(echo -n "$GIT_HASH" | head -c 7)"
|
||||||
|
fi
|
||||||
|
IB_OS_ENV="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/ENVIRONMENT.txt)"
|
||||||
|
IB_TARGET_PLATFORM="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/PLATFORM.txt)"
|
||||||
|
|
||||||
|
VERSION_FULL="${VERSION}-${GIT_HASH}"
|
||||||
|
if [ -n "$IB_OS_ENV" ]; then
|
||||||
|
VERSION_FULL="$VERSION_FULL~${IB_OS_ENV}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
IMAGE_BASENAME=startos-${VERSION_FULL}_${IB_TARGET_PLATFORM}
|
||||||
|
|
||||||
|
BOOTLOADERS=grub-efi
|
||||||
|
if [ "$IB_TARGET_PLATFORM" = "x86_64" ] || [ "$IB_TARGET_PLATFORM" = "x86_64-nonfree" ]; then
|
||||||
|
IB_TARGET_ARCH=amd64
|
||||||
|
QEMU_ARCH=x86_64
|
||||||
|
BOOTLOADERS=grub-efi,syslinux
|
||||||
|
elif [ "$IB_TARGET_PLATFORM" = "aarch64" ] || [ "$IB_TARGET_PLATFORM" = "aarch64-nonfree" ] || [ "$IB_TARGET_PLATFORM" = "raspberrypi" ] || [ "$IB_TARGET_PLATFORM" = "rockchip64" ]; then
|
||||||
|
IB_TARGET_ARCH=arm64
|
||||||
|
QEMU_ARCH=aarch64
|
||||||
|
elif [ "$IB_TARGET_PLATFORM" = "riscv64" ] || [ "$IB_TARGET_PLATFORM" = "riscv64-nonfree" ]; then
|
||||||
|
IB_TARGET_ARCH=riscv64
|
||||||
|
QEMU_ARCH=riscv64
|
||||||
|
else
|
||||||
|
IB_TARGET_ARCH="$IB_TARGET_PLATFORM"
|
||||||
|
QEMU_ARCH="$IB_TARGET_PLATFORM"
|
||||||
|
fi
|
||||||
|
|
||||||
|
QEMU_ARGS=()
|
||||||
|
if [ "$QEMU_ARCH" != $(uname -m) ]; then
|
||||||
|
QEMU_ARGS+=(--bootstrap-qemu-arch ${IB_TARGET_ARCH})
|
||||||
|
QEMU_ARGS+=(--bootstrap-qemu-static /usr/bin/qemu-${QEMU_ARCH}-static)
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p $prep_results_dir
|
||||||
|
|
||||||
|
cd $prep_results_dir
|
||||||
|
|
||||||
|
NON_FREE=
|
||||||
|
if [[ "${IB_TARGET_PLATFORM}" =~ -nonfree$ ]] || [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
|
||||||
|
NON_FREE=1
|
||||||
|
fi
|
||||||
|
IMAGE_TYPE=iso
|
||||||
|
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ] || [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then
|
||||||
|
IMAGE_TYPE=img
|
||||||
|
fi
|
||||||
|
|
||||||
|
ARCHIVE_AREAS="main contrib"
|
||||||
|
if [ "$NON_FREE" = 1 ]; then
|
||||||
|
if [ "$IB_SUITE" = "bullseye" ]; then
|
||||||
|
ARCHIVE_AREAS="$ARCHIVE_AREAS non-free"
|
||||||
|
else
|
||||||
|
ARCHIVE_AREAS="$ARCHIVE_AREAS non-free non-free-firmware"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
PLATFORM_CONFIG_EXTRAS=()
|
||||||
|
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
|
||||||
|
PLATFORM_CONFIG_EXTRAS+=( --firmware-binary false )
|
||||||
|
PLATFORM_CONFIG_EXTRAS+=( --firmware-chroot false )
|
||||||
|
RPI_KERNEL_VERSION=6.12.47+rpt
|
||||||
|
PLATFORM_CONFIG_EXTRAS+=( --linux-packages linux-image-$RPI_KERNEL_VERSION )
|
||||||
|
PLATFORM_CONFIG_EXTRAS+=( --linux-flavours "rpi-v8 rpi-2712" )
|
||||||
|
elif [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then
|
||||||
|
PLATFORM_CONFIG_EXTRAS+=( --linux-flavours rockchip64 )
|
||||||
|
elif [ "${IB_TARGET_ARCH}" = "riscv64" ]; then
|
||||||
|
PLATFORM_CONFIG_EXTRAS+=( --uefi-secure-boot=disable )
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
cat > /etc/wgetrc << EOF
|
||||||
|
retry_connrefused = on
|
||||||
|
tries = 100
|
||||||
|
EOF
|
||||||
|
lb config \
|
||||||
|
--iso-application "StartOS v${VERSION_FULL} ${IB_TARGET_ARCH}" \
|
||||||
|
--iso-volume "StartOS v${VERSION} ${IB_TARGET_ARCH}" \
|
||||||
|
--iso-preparer "START9 LABS; HTTPS://START9.COM" \
|
||||||
|
--iso-publisher "START9 LABS; HTTPS://START9.COM" \
|
||||||
|
--backports true \
|
||||||
|
--bootappend-live "boot=live noautologin" \
|
||||||
|
--bootloaders $BOOTLOADERS \
|
||||||
|
--cache false \
|
||||||
|
--mirror-bootstrap "https://deb.debian.org/debian/" \
|
||||||
|
--mirror-chroot "https://deb.debian.org/debian/" \
|
||||||
|
--mirror-chroot-security "https://security.debian.org/debian-security" \
|
||||||
|
-d ${IB_SUITE} \
|
||||||
|
-a ${IB_TARGET_ARCH} \
|
||||||
|
${QEMU_ARGS[@]} \
|
||||||
|
--archive-areas "${ARCHIVE_AREAS}" \
|
||||||
|
${PLATFORM_CONFIG_EXTRAS[@]}
|
||||||
|
|
||||||
|
# Overlays
|
||||||
|
|
||||||
|
mkdir -p config/packages.chroot/
|
||||||
|
cp $RESULTS_DIR/$IMAGE_BASENAME.deb config/packages.chroot/
|
||||||
|
dpkg-name config/packages.chroot/*.deb
|
||||||
|
|
||||||
|
mkdir -p config/includes.chroot/etc
|
||||||
|
echo start > config/includes.chroot/etc/hostname
|
||||||
|
cat > config/includes.chroot/etc/hosts << EOT
|
||||||
|
127.0.0.1 localhost start
|
||||||
|
::1 localhost start ip6-localhost ip6-loopback
|
||||||
|
ff02::1 ip6-allnodes
|
||||||
|
ff02::2 ip6-allrouters
|
||||||
|
EOT
|
||||||
|
|
||||||
|
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
|
||||||
|
mkdir -p config/includes.chroot
|
||||||
|
git clone --depth=1 --branch=stable https://github.com/raspberrypi/rpi-firmware.git config/includes.chroot/boot
|
||||||
|
rm -rf config/includes.chroot/boot/.git config/includes.chroot/boot/modules
|
||||||
|
rsync -rLp $SOURCE_DIR/raspberrypi/squashfs/ config/includes.chroot/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bootloaders
|
||||||
|
|
||||||
|
rm -rf config/bootloaders
|
||||||
|
cp -r /usr/share/live/build/bootloaders config/bootloaders
|
||||||
|
|
||||||
|
cat > config/bootloaders/syslinux/syslinux.cfg << EOF
|
||||||
|
include menu.cfg
|
||||||
|
default vesamenu.c32
|
||||||
|
prompt 0
|
||||||
|
timeout 50
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > config/bootloaders/isolinux/isolinux.cfg << EOF
|
||||||
|
include menu.cfg
|
||||||
|
default vesamenu.c32
|
||||||
|
prompt 0
|
||||||
|
timeout 50
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Extract splash.png from the deb package
|
||||||
|
dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xf - ./usr/lib/startos/splash.png > /tmp/splash.png
|
||||||
|
cp /tmp/splash.png config/bootloaders/syslinux_common/splash.png
|
||||||
|
cp /tmp/splash.png config/bootloaders/isolinux/splash.png
|
||||||
|
cp /tmp/splash.png config/bootloaders/grub-pc/splash.png
|
||||||
|
rm /tmp/splash.png
|
||||||
|
|
||||||
|
sed -i -e '2i set timeout=5' config/bootloaders/grub-pc/config.cfg
|
||||||
|
|
||||||
|
# Archives
|
||||||
|
|
||||||
|
mkdir -p config/archives
|
||||||
|
|
||||||
|
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
|
||||||
|
curl -fsSL https://archive.raspberrypi.com/debian/raspberrypi.gpg.key | gpg --dearmor -o config/archives/raspi.key
|
||||||
|
echo "deb [arch=${IB_TARGET_ARCH} signed-by=/etc/apt/trusted.gpg.d/raspi.key.gpg] https://archive.raspberrypi.com/debian/ ${IB_SUITE} main" > config/archives/raspi.list
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then
|
||||||
|
curl -fsSL https://apt.armbian.com/armbian.key | gpg --dearmor -o config/archives/armbian.key
|
||||||
|
echo "deb https://apt.armbian.com/ ${IB_SUITE} main" > config/archives/armbian.list
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$NON_FREE" = 1 ]; then
|
||||||
|
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --dearmor -o config/archives/nvidia-container-toolkit.key
|
||||||
|
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
|
||||||
|
| sed 's#deb https://#deb [signed-by=/etc/apt/trusted.gpg.d/nvidia-container-toolkit.key.gpg] https://#g' \
|
||||||
|
> config/archives/nvidia-container-toolkit.list
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > config/archives/backports.pref <<-EOF
|
||||||
|
Package: linux-image-*
|
||||||
|
Pin: release n=${IB_SUITE}-backports
|
||||||
|
Pin-Priority: 500
|
||||||
|
|
||||||
|
Package: linux-headers-*
|
||||||
|
Pin: release n=${IB_SUITE}-backports
|
||||||
|
Pin-Priority: 500
|
||||||
|
|
||||||
|
Package: *nvidia*
|
||||||
|
Pin: release n=${IB_SUITE}-backports
|
||||||
|
Pin-Priority: 500
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Hooks
|
||||||
|
|
||||||
|
cat > config/hooks/normal/9000-install-startos.hook.chroot << EOF
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "${NON_FREE}" = "1" ] && [ "${IB_TARGET_PLATFORM}" != "raspberrypi" ] && [ "${IB_TARGET_PLATFORM}" != "riscv64-nonfree" ]; then
|
||||||
|
# install a specific NVIDIA driver version
|
||||||
|
|
||||||
|
# ---------------- configuration ----------------
|
||||||
|
NVIDIA_DRIVER_VERSION="\${NVIDIA_DRIVER_VERSION:-580.119.02}"
|
||||||
|
|
||||||
|
BASE_URL="https://download.nvidia.com/XFree86/Linux-${QEMU_ARCH}"
|
||||||
|
|
||||||
|
echo "[nvidia-hook] Using NVIDIA driver: \${NVIDIA_DRIVER_VERSION}" >&2
|
||||||
|
|
||||||
|
# ---------------- kernel version ----------------
|
||||||
|
|
||||||
|
# Determine target kernel version from newest /boot/vmlinuz-* in the chroot.
|
||||||
|
KVER="\$(
|
||||||
|
ls -1t /boot/vmlinuz-* 2>/dev/null \
|
||||||
|
| head -n1 \
|
||||||
|
| sed 's|.*/vmlinuz-||'
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [ -z "\${KVER}" ]; then
|
||||||
|
echo "[nvidia-hook] ERROR: no /boot/vmlinuz-* found; cannot determine kernel version" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[nvidia-hook] Target kernel version: \${KVER}" >&2
|
||||||
|
|
||||||
|
# Ensure kernel headers are present
|
||||||
|
TEMP_APT_DEPS=(build-essential)
|
||||||
|
if [ ! -e "/lib/modules/\${KVER}/build" ]; then
|
||||||
|
TEMP_APT_DEPS+=(linux-headers-\${KVER})
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[nvidia-hook] Installing build dependencies" >&2
|
||||||
|
|
||||||
|
/usr/lib/startos/scripts/install-equivs <<-EOF
|
||||||
|
Package: nvidia-depends
|
||||||
|
Version: \${NVIDIA_DRIVER_VERSION}
|
||||||
|
Section: unknown
|
||||||
|
Priority: optional
|
||||||
|
Depends: \${dep_list="\$(IFS=', '; echo "\${TEMP_APT_DEPS[*]}")"}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ---------------- download and run installer ----------------
|
||||||
|
|
||||||
|
RUN_NAME="NVIDIA-Linux-${QEMU_ARCH}-\${NVIDIA_DRIVER_VERSION}.run"
|
||||||
|
RUN_PATH="/root/\${RUN_NAME}"
|
||||||
|
RUN_URL="\${BASE_URL}/\${NVIDIA_DRIVER_VERSION}/\${RUN_NAME}"
|
||||||
|
|
||||||
|
echo "[nvidia-hook] Downloading \${RUN_URL}" >&2
|
||||||
|
wget -O "\${RUN_PATH}" "\${RUN_URL}"
|
||||||
|
chmod +x "\${RUN_PATH}"
|
||||||
|
|
||||||
|
echo "[nvidia-hook] Running NVIDIA installer for kernel \${KVER}" >&2
|
||||||
|
|
||||||
|
sh "\${RUN_PATH}" \
|
||||||
|
--silent \
|
||||||
|
--kernel-name="\${KVER}" \
|
||||||
|
--no-x-check \
|
||||||
|
--no-nouveau-check \
|
||||||
|
--no-runlevel-check
|
||||||
|
|
||||||
|
# Rebuild module metadata
|
||||||
|
echo "[nvidia-hook] Running depmod for \${KVER}" >&2
|
||||||
|
depmod -a "\${KVER}"
|
||||||
|
|
||||||
|
echo "[nvidia-hook] NVIDIA \${NVIDIA_DRIVER_VERSION} installation complete for kernel \${KVER}" >&2
|
||||||
|
|
||||||
|
echo "[nvidia-hook] Removing build dependencies..." >&2
|
||||||
|
apt-get purge -y nvidia-depends
|
||||||
|
apt-get autoremove -y
|
||||||
|
echo "[nvidia-hook] Removed build dependencies." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp /etc/resolv.conf /etc/resolv.conf.bak
|
||||||
|
|
||||||
|
if [ "${IB_SUITE}" = trixie ] && [ "${IB_TARGET_ARCH}" != riscv64 ]; then
|
||||||
|
echo 'deb https://deb.debian.org/debian/ bookworm main' > /etc/apt/sources.list.d/bookworm.list
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y postgresql-15
|
||||||
|
rm /etc/apt/sources.list.d/bookworm.list
|
||||||
|
apt-get update
|
||||||
|
systemctl mask postgresql
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
|
||||||
|
ln -sf /usr/bin/pi-beep /usr/local/bin/beep
|
||||||
|
KERNEL_VERSION=${RPI_KERNEL_VERSION} sh /boot/config.sh > /boot/config.txt
|
||||||
|
mkinitramfs -c gzip -o /boot/initrd.img-${RPI_KERNEL_VERSION}-rpi-v8 ${RPI_KERNEL_VERSION}-rpi-v8
|
||||||
|
mkinitramfs -c gzip -o /boot/initrd.img-${RPI_KERNEL_VERSION}-rpi-2712 ${RPI_KERNEL_VERSION}-rpi-2712
|
||||||
|
fi
|
||||||
|
|
||||||
|
useradd --shell /bin/bash -G startos -m start9
|
||||||
|
echo start9:embassy | chpasswd
|
||||||
|
usermod -aG sudo start9
|
||||||
|
usermod -aG systemd-journal start9
|
||||||
|
|
||||||
|
echo "start9 ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/010_start9-nopasswd"
|
||||||
|
|
||||||
|
if [ "${IB_TARGET_PLATFORM}" != "raspberrypi" ]; then
|
||||||
|
/usr/lib/startos/scripts/enable-kiosk
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [[ "${IB_OS_ENV}" =~ (^|-)dev($|-) ]]; then
|
||||||
|
passwd -l start9
|
||||||
|
fi
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(date '+%s')}"
|
||||||
|
|
||||||
|
if lb bootstrap; then
|
||||||
|
true
|
||||||
|
else
|
||||||
|
EXIT=$?
|
||||||
|
cat ./chroot/debootstrap/debootstrap.log
|
||||||
|
exit $EXIT
|
||||||
|
fi
|
||||||
|
lb chroot
|
||||||
|
lb installer
|
||||||
|
lb binary_chroot
|
||||||
|
lb chroot_prep install all mode-apt-install-binary mode-archives-chroot
|
||||||
|
mv chroot/chroot/etc/resolv.conf.bak chroot/chroot/etc/resolv.conf
|
||||||
|
lb binary_rootfs
|
||||||
|
|
||||||
|
cp $prep_results_dir/binary/live/filesystem.squashfs $RESULTS_DIR/$IMAGE_BASENAME.squashfs
|
||||||
|
|
||||||
|
if [ "${IMAGE_TYPE}" = iso ]; then
|
||||||
|
|
||||||
|
lb binary_manifest
|
||||||
|
lb binary_package-lists
|
||||||
|
lb binary_linux-image
|
||||||
|
lb binary_memtest
|
||||||
|
lb binary_grub-legacy
|
||||||
|
lb binary_grub-pc
|
||||||
|
lb binary_grub_cfg
|
||||||
|
lb binary_syslinux
|
||||||
|
lb binary_disk
|
||||||
|
lb binary_loadlin
|
||||||
|
lb binary_win32-loader
|
||||||
|
lb binary_includes
|
||||||
|
lb binary_grub-efi
|
||||||
|
lb binary_hooks
|
||||||
|
lb binary_checksums
|
||||||
|
find binary -newermt "$(date -d@${SOURCE_DATE_EPOCH} '+%Y-%m-%d %H:%M:%S')" -printf "%y %p\n" -exec touch '{}' -d@${SOURCE_DATE_EPOCH} --no-dereference ';' > binary.modified_timestamps
|
||||||
|
lb binary_iso
|
||||||
|
lb binary_onie
|
||||||
|
lb binary_netboot
|
||||||
|
lb binary_tar
|
||||||
|
lb binary_hdd
|
||||||
|
lb binary_zsync
|
||||||
|
lb chroot_prep remove all mode-archives-chroot
|
||||||
|
lb source
|
||||||
|
|
||||||
|
mv $prep_results_dir/live-image-${IB_TARGET_ARCH}.hybrid.iso $RESULTS_DIR/$IMAGE_BASENAME.iso
|
||||||
|
|
||||||
|
elif [ "${IMAGE_TYPE}" = img ]; then
|
||||||
|
|
||||||
|
SECTOR_LEN=512
|
||||||
|
BOOT_START=$((1024 * 1024)) # 1MiB
|
||||||
|
BOOT_LEN=$((512 * 1024 * 1024)) # 512MiB
|
||||||
|
BOOT_END=$((BOOT_START + BOOT_LEN - 1))
|
||||||
|
ROOT_START=$((BOOT_END + 1))
|
||||||
|
ROOT_LEN=$((MAX_IMG_LEN - ROOT_START))
|
||||||
|
ROOT_END=$((MAX_IMG_LEN - 1))
|
||||||
|
|
||||||
|
TARGET_NAME=$prep_results_dir/${IMAGE_BASENAME}.img
|
||||||
|
truncate -s $MAX_IMG_LEN $TARGET_NAME
|
||||||
|
|
||||||
|
sfdisk $TARGET_NAME <<-EOF
|
||||||
|
label: dos
|
||||||
|
label-id: 0xcb15ae4d
|
||||||
|
unit: sectors
|
||||||
|
sector-size: 512
|
||||||
|
|
||||||
|
${TARGET_NAME}1 : start=$((BOOT_START / SECTOR_LEN)), size=$((BOOT_LEN / SECTOR_LEN)), type=c, bootable
|
||||||
|
${TARGET_NAME}2 : start=$((ROOT_START / SECTOR_LEN)), size=$((ROOT_LEN / SECTOR_LEN)), type=83
|
||||||
|
EOF
|
||||||
|
|
||||||
|
BOOT_DEV=$(losetup --show -f --offset $BOOT_START --sizelimit $BOOT_LEN $TARGET_NAME)
|
||||||
|
ROOT_DEV=$(losetup --show -f --offset $ROOT_START --sizelimit $ROOT_LEN $TARGET_NAME)
|
||||||
|
|
||||||
|
mkfs.vfat -F32 $BOOT_DEV
|
||||||
|
mkfs.ext4 $ROOT_DEV
|
||||||
|
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
|
||||||
|
mkdir -p $TMPDIR/boot $TMPDIR/root
|
||||||
|
mount $ROOT_DEV $TMPDIR/root
|
||||||
|
mount $BOOT_DEV $TMPDIR/boot
|
||||||
|
unsquashfs -n -f -d $TMPDIR $prep_results_dir/binary/live/filesystem.squashfs boot
|
||||||
|
|
||||||
|
mkdir $TMPDIR/root/images $TMPDIR/root/config
|
||||||
|
B3SUM=$(b3sum $prep_results_dir/binary/live/filesystem.squashfs | head -c 16)
|
||||||
|
cp $prep_results_dir/binary/live/filesystem.squashfs $TMPDIR/root/images/$B3SUM.rootfs
|
||||||
|
ln -rsf $TMPDIR/root/images/$B3SUM.rootfs $TMPDIR/root/config/current.rootfs
|
||||||
|
|
||||||
|
mkdir -p $TMPDIR/next $TMPDIR/lower $TMPDIR/root/config/work $TMPDIR/root/config/overlay
|
||||||
|
mount $TMPDIR/root/config/current.rootfs $TMPDIR/lower
|
||||||
|
|
||||||
|
mount -t overlay -o lowerdir=$TMPDIR/lower,workdir=$TMPDIR/root/config/work,upperdir=$TMPDIR/root/config/overlay overlay $TMPDIR/next
|
||||||
|
|
||||||
|
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
|
||||||
|
sed -i 's| boot=startos| boot=startos init=/usr/lib/startos/scripts/init_resize\.sh|' $TMPDIR/boot/cmdline.txt
|
||||||
|
rsync -a $SOURCE_DIR/raspberrypi/img/ $TMPDIR/next/
|
||||||
|
fi
|
||||||
|
|
||||||
|
umount $TMPDIR/next
|
||||||
|
umount $TMPDIR/lower
|
||||||
|
|
||||||
|
umount $TMPDIR/boot
|
||||||
|
umount $TMPDIR/root
|
||||||
|
|
||||||
|
|
||||||
|
e2fsck -fy $ROOT_DEV
|
||||||
|
resize2fs -M $ROOT_DEV
|
||||||
|
|
||||||
|
BLOCK_COUNT=$(dumpe2fs -h $ROOT_DEV | awk '/^Block count:/ { print $3 }')
|
||||||
|
BLOCK_SIZE=$(dumpe2fs -h $ROOT_DEV | awk '/^Block size:/ { print $3 }')
|
||||||
|
ROOT_LEN=$((BLOCK_COUNT * BLOCK_SIZE))
|
||||||
|
|
||||||
|
losetup -d $ROOT_DEV
|
||||||
|
losetup -d $BOOT_DEV
|
||||||
|
|
||||||
|
# Recreate partition 2 with the new size using sfdisk
|
||||||
|
sfdisk $TARGET_NAME <<-EOF
|
||||||
|
label: dos
|
||||||
|
label-id: 0xcb15ae4d
|
||||||
|
unit: sectors
|
||||||
|
sector-size: 512
|
||||||
|
|
||||||
|
${TARGET_NAME}1 : start=$((BOOT_START / SECTOR_LEN)), size=$((BOOT_LEN / SECTOR_LEN)), type=c, bootable
|
||||||
|
${TARGET_NAME}2 : start=$((ROOT_START / SECTOR_LEN)), size=$((ROOT_LEN / SECTOR_LEN)), type=83
|
||||||
|
EOF
|
||||||
|
|
||||||
|
TARGET_SIZE=$((ROOT_START + ROOT_LEN))
|
||||||
|
truncate -s $TARGET_SIZE $TARGET_NAME
|
||||||
|
|
||||||
|
mv $TARGET_NAME $RESULTS_DIR/$IMAGE_BASENAME.img
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
chown $IB_UID:$IB_UID $RESULTS_DIR/$IMAGE_BASENAME.*
|
||||||
@@ -1 +1 @@
|
|||||||
usb-storage.quirks=152d:0562:u,14cd:121c:u,0781:cfcb:u console=serial0,115200 console=tty1 root=PARTUUID=cb15ae4d-02 rootfstype=ext4 fsck.repair=yes rootwait cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory quiet boot=startos
|
usb-storage.quirks=152d:0562:u,14cd:121c:u,0781:cfcb:u console=serial0,115200 console=tty1 root=PARTUUID=cb15ae4d-02 rootfstype=ext4 fsck.repair=yes rootwait cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory boot=startos
|
||||||
46
build/image-recipe/raspberrypi/squashfs/boot/config.sh
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
cat << EOF
|
||||||
|
|
||||||
|
# Enable audio (loads snd_bcm2835)
|
||||||
|
dtparam=audio=on
|
||||||
|
|
||||||
|
# Automatically load overlays for detected cameras
|
||||||
|
camera_auto_detect=1
|
||||||
|
|
||||||
|
# Automatically load overlays for detected DSI displays
|
||||||
|
display_auto_detect=1
|
||||||
|
|
||||||
|
# Enable DRM VC4 V3D driver
|
||||||
|
dtoverlay=vc4-kms-v3d
|
||||||
|
max_framebuffers=2
|
||||||
|
|
||||||
|
# Run in 64-bit mode
|
||||||
|
arm_64bit=1
|
||||||
|
|
||||||
|
# Disable compensation for displays with overscan
|
||||||
|
disable_overscan=1
|
||||||
|
|
||||||
|
[cm4]
|
||||||
|
# Enable host mode on the 2711 built-in XHCI USB controller.
|
||||||
|
# This line should be removed if the legacy DWC2 controller is required
|
||||||
|
# (e.g. for USB device mode) or if USB support is not required.
|
||||||
|
otg_mode=1
|
||||||
|
|
||||||
|
[all]
|
||||||
|
|
||||||
|
[pi4]
|
||||||
|
# Run as fast as firmware / board allows
|
||||||
|
arm_boost=1
|
||||||
|
kernel=vmlinuz-${KERNEL_VERSION}-rpi-v8
|
||||||
|
initramfs initrd.img-${KERNEL_VERSION}-rpi-v8 followkernel
|
||||||
|
|
||||||
|
[pi5]
|
||||||
|
kernel=vmlinuz-${KERNEL_VERSION}-rpi-2712
|
||||||
|
initramfs initrd.img-${KERNEL_VERSION}-rpi-2712 followkernel
|
||||||
|
|
||||||
|
[all]
|
||||||
|
gpu_mem=16
|
||||||
|
dtoverlay=pwm-2chan,disable-bt
|
||||||
|
|
||||||
|
EOF
|
||||||
35
build/image-recipe/run-local-build.sh
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
|
||||||
|
|
||||||
|
BASEDIR="$(pwd -P)"
|
||||||
|
|
||||||
|
SUITE=trixie
|
||||||
|
|
||||||
|
USE_TTY=
|
||||||
|
if tty -s; then
|
||||||
|
USE_TTY="-it"
|
||||||
|
fi
|
||||||
|
|
||||||
|
dockerfile_hash=$(sha256sum ${BASEDIR}/build/image-recipe/Dockerfile | head -c 7)
|
||||||
|
|
||||||
|
docker_img_name="start9/build-iso:${SUITE}-${dockerfile_hash}"
|
||||||
|
|
||||||
|
platform=linux/${ARCH}
|
||||||
|
case $ARCH in
|
||||||
|
x86_64)
|
||||||
|
platform=linux/amd64;;
|
||||||
|
aarch64)
|
||||||
|
platform=linux/arm64;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if ! docker run --rm --platform=$platform "${docker_img_name}" true 2> /dev/null; then
|
||||||
|
docker buildx build --load --platform=$platform --build-arg=SUITE=${SUITE} -t "${docker_img_name}" ./build/image-recipe
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run $USE_TTY --rm --platform=$platform --privileged -v "$(pwd)/build/image-recipe:/root/image-recipe" -v "$(pwd)/results:/root/results" \
|
||||||
|
-e IB_SUITE="$SUITE" \
|
||||||
|
-e IB_UID="$UID" \
|
||||||
|
-e IB_INCLUDE \
|
||||||
|
"${docker_img_name}" /root/image-recipe/build.sh $@
|
||||||
51
build/lib/grub-theme/theme.txt
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
desktop-image: "../splash.png"
|
||||||
|
title-color: "#ffffff"
|
||||||
|
title-font: "Unifont Regular 16"
|
||||||
|
title-text: "StartOS Boot Menu with GRUB"
|
||||||
|
message-font: "Unifont Regular 16"
|
||||||
|
terminal-font: "Unifont Regular 16"
|
||||||
|
|
||||||
|
#help bar at the bottom
|
||||||
|
+ label {
|
||||||
|
top = 100%-50
|
||||||
|
left = 0
|
||||||
|
width = 100%
|
||||||
|
height = 20
|
||||||
|
text = "@KEYMAP_SHORT@"
|
||||||
|
align = "center"
|
||||||
|
color = "#ffffff"
|
||||||
|
font = "Unifont Regular 16"
|
||||||
|
}
|
||||||
|
|
||||||
|
#boot menu
|
||||||
|
+ boot_menu {
|
||||||
|
left = 10%
|
||||||
|
width = 80%
|
||||||
|
top = 52%
|
||||||
|
height = 48%-80
|
||||||
|
item_color = "#a8a8a8"
|
||||||
|
item_font = "Unifont Regular 16"
|
||||||
|
selected_item_color= "#ffffff"
|
||||||
|
selected_item_font = "Unifont Regular 16"
|
||||||
|
item_height = 16
|
||||||
|
item_padding = 0
|
||||||
|
item_spacing = 4
|
||||||
|
icon_width = 0
|
||||||
|
icon_heigh = 0
|
||||||
|
item_icon_space = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress bar
|
||||||
|
+ progress_bar {
|
||||||
|
id = "__timeout__"
|
||||||
|
left = 15%
|
||||||
|
top = 100%-80
|
||||||
|
height = 16
|
||||||
|
width = 70%
|
||||||
|
font = "Unifont Regular 16"
|
||||||
|
text_color = "#000000"
|
||||||
|
fg_color = "#ffffff"
|
||||||
|
bg_color = "#a8a8a8"
|
||||||
|
border_color = "#ffffff"
|
||||||
|
text = "@TIMEOUT_NOTIFICATION_LONG@"
|
||||||
|
}
|
||||||
147
build/lib/motd
@@ -1,34 +1,123 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
printf "\n"
|
|
||||||
printf "Welcome to\n"
|
|
||||||
cat << "ASCII"
|
|
||||||
|
|
||||||
███████
|
parse_essential_db_info() {
|
||||||
█ █ █
|
DB_DUMP="/tmp/startos_db.json"
|
||||||
█ █ █ █
|
|
||||||
█ █ █ █
|
|
||||||
█ █ █ █
|
|
||||||
█ █ █ █
|
|
||||||
█ █
|
|
||||||
███████
|
|
||||||
|
|
||||||
_____ __ ___ __ __
|
if command -v start-cli >/dev/null 2>&1; then
|
||||||
(_ | /\ |__) | / \(_
|
timeout 30 start-cli db dump > "$DB_DUMP" 2>/dev/null || return 1
|
||||||
__) | / \| \ | \__/__)
|
else
|
||||||
ASCII
|
return 1
|
||||||
printf " v$(cat /usr/lib/startos/VERSION.txt)\n\n"
|
fi
|
||||||
printf " %s (%s %s)\n" "$(uname -o)" "$(uname -r)" "$(uname -m)"
|
|
||||||
printf " Git Hash: $(cat /usr/lib/startos/GIT_HASH.txt)"
|
if command -v jq >/dev/null 2>&1 && [ -f "$DB_DUMP" ]; then
|
||||||
if [ -n "$(cat /usr/lib/startos/ENVIRONMENT.txt)" ]; then
|
HOSTNAME=$(jq -r '.value.serverInfo.hostname // "unknown"' "$DB_DUMP" 2>/dev/null)
|
||||||
printf " ~ $(cat /usr/lib/startos/ENVIRONMENT.txt)\n"
|
VERSION=$(jq -r '.value.serverInfo.version // "unknown"' "$DB_DUMP" 2>/dev/null)
|
||||||
else
|
RAM_BYTES=$(jq -r '.value.serverInfo.ram // 0' "$DB_DUMP" 2>/dev/null)
|
||||||
printf "\n"
|
WAN_IP=$(jq -r '.value.serverInfo.network.gateways[].ipInfo.wanIp // "unknown"' "$DB_DUMP" 2>/dev/null | head -1)
|
||||||
|
NTP_SYNCED=$(jq -r '.value.serverInfo.ntpSynced // false' "$DB_DUMP" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$RAM_BYTES" != "0" ] && [ "$RAM_BYTES" != "null" ]; then
|
||||||
|
RAM_GB=$(echo "scale=1; $RAM_BYTES / 1073741824" | bc 2>/dev/null || echo "unknown")
|
||||||
|
else
|
||||||
|
RAM_GB="unknown"
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUNNING_SERVICES=$(jq -r '[.value.packageData[] | select(.statusInfo.started != null)] | length' "$DB_DUMP" 2>/dev/null)
|
||||||
|
TOTAL_SERVICES=$(jq -r '.value.packageData | length' "$DB_DUMP" 2>/dev/null)
|
||||||
|
|
||||||
|
rm -f "$DB_DUMP"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
rm -f "$DB_DUMP" 2>/dev/null
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
DB_INFO_AVAILABLE=0
|
||||||
|
if parse_essential_db_info; then
|
||||||
|
DB_INFO_AVAILABLE=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf "\n"
|
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$VERSION" != "unknown" ]; then
|
||||||
printf " * Documentation: https://docs.start9.com\n"
|
version_display="v$VERSION"
|
||||||
printf " * Management: https://%s.local\n" "$(hostname)"
|
else
|
||||||
printf " * Support: https://start9.com/contact\n"
|
version_display="v$(cat /usr/lib/startos/VERSION.txt 2>/dev/null || echo 'unknown')"
|
||||||
printf " * Source Code: https://github.com/Start9Labs/start-os\n"
|
fi
|
||||||
printf " * License: MIT\n"
|
|
||||||
printf "\n"
|
printf "\n\033[1;37m ▄▄▀▀▀▀▀▄▄\033[0m\n"
|
||||||
|
printf "\033[1;37m ▄▀ ▄ ▀▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄ \033[1;31m▄██████▄ ▄██████\033[0m\n"
|
||||||
|
printf "\033[1;37m █ █ █ █ █ █ █ █ █ ▀▄ █ \033[1;31m██ ██ ██ \033[0m\n"
|
||||||
|
printf "\033[1;37m█ █ █ █ ▀▄▄▄▄ █ █ █ █ ▄▄▄▀ █ \033[1;31m██ ██ ▀█████▄\033[0m\n"
|
||||||
|
printf "\033[1;37m█ █ █ █ █ █ █ █ █ ▀▄ █ \033[1;31m██ ██ ██\033[0m\n"
|
||||||
|
printf "\033[1;37m █ █ █ █ ▄▄▄▄▄▀ █ █ █ █ ▀▄ █ \033[1;31m▀██████▀ ██████▀\033[0m\n"
|
||||||
|
printf "\033[1;37m █ █\033[0m\n"
|
||||||
|
printf "\033[1;37m ▀▀▄▄▄▀▀ $version_display\033[0m\n\n"
|
||||||
|
|
||||||
|
uptime_str=$(uptime | awk -F'up ' '{print $2}' | awk -F',' '{print $1}' | sed 's/^ *//')
|
||||||
|
|
||||||
|
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$RAM_GB" != "unknown" ]; then
|
||||||
|
memory_used=$(free -m | awk 'NR==2{printf "%.0fMB", $3}')
|
||||||
|
memory_display="$memory_used / ${RAM_GB}GB"
|
||||||
|
else
|
||||||
|
memory_display=$(free -m | awk 'NR==2{printf "%.0fMB / %.0fMB", $3, $2}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
root_usage=$(df -h / | awk 'NR==2{printf "%s (%s free)", $5, $4}')
|
||||||
|
|
||||||
|
if [ -d "/media/startos/data/package-data" ]; then
|
||||||
|
data_usage=$(df -h /media/startos/data/package-data | awk 'NR==2{printf "%s (%s free)", $5, $4}')
|
||||||
|
else
|
||||||
|
data_usage="N/A"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DB_INFO_AVAILABLE" -eq 1 ]; then
|
||||||
|
services_text="$RUNNING_SERVICES/$TOTAL_SERVICES running"
|
||||||
|
else
|
||||||
|
services_text="Unknown"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -1)
|
||||||
|
if [ -z "$local_ip" ]; then local_ip="N/A"; fi
|
||||||
|
|
||||||
|
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$WAN_IP" != "unknown" ]; then
|
||||||
|
wan_ip="$WAN_IP"
|
||||||
|
else
|
||||||
|
wan_ip="N/A"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf " \033[1;37m┌─ SYSTEM STATUS ───────────────────────────────────────────────────┐\033[0m\n"
|
||||||
|
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Uptime:" "$uptime_str" "Memory:" "$memory_display"
|
||||||
|
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Root:" "$root_usage" "Data:" "$data_usage"
|
||||||
|
|
||||||
|
if [ "$DB_INFO_AVAILABLE" -eq 1 ]; then
|
||||||
|
if [ "$RUNNING_SERVICES" -eq "$TOTAL_SERVICES" ] && [ "$TOTAL_SERVICES" -gt 0 ]; then
|
||||||
|
printf " \033[1;37m│\033[0m %-8s \033[0;32m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
|
||||||
|
elif [ "$RUNNING_SERVICES" -gt 0 ]; then
|
||||||
|
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
|
||||||
|
else
|
||||||
|
printf " \033[1;37m│\033[0m %-8s \033[0;31m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf " \033[1;37m│\033[0m %-8s \033[0;37m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$NTP_SYNCED" = "true" ]; then
|
||||||
|
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;32m%-23s\033[0m \033[1;37m│\033[0m\n" "Local:" "$local_ip" "NTP:" "Synced"
|
||||||
|
elif [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$NTP_SYNCED" = "false" ]; then
|
||||||
|
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;31m%-23s\033[0m \033[1;37m│\033[0m\n" "Local:" "$local_ip" "NTP:" "Not Synced"
|
||||||
|
else
|
||||||
|
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;37m%-23s\033[0m \033[1;37m│\033[0m\n" "Local:" "$local_ip" "NTP:" "Unknown"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf " \033[1;37m└───────────────────────────────────────────────────────────────────┘\033[0m"
|
||||||
|
|
||||||
|
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$HOSTNAME" != "unknown" ]; then
|
||||||
|
web_url="https://$HOSTNAME.local"
|
||||||
|
else
|
||||||
|
web_url="https://$(hostname).local"
|
||||||
|
fi
|
||||||
|
printf "\n \033[1;37m┌──────────────────────────────────────────────────── QUICK ACCESS ─┐\033[0m\n"
|
||||||
|
printf " \033[1;37m│\033[0m Web Interface: \033[0;36m%-50s\033[0m \033[1;37m│\033[0m\n" "$web_url"
|
||||||
|
printf " \033[1;37m│\033[0m Documentation: \033[0;36m%-50s\033[0m \033[1;37m│\033[0m\n" "https://staging.docs.start9.com"
|
||||||
|
printf " \033[1;37m│\033[0m Support: \033[0;36m%-50s\033[0m \033[1;37m│\033[0m\n" "https://start9.com/contact"
|
||||||
|
printf " \033[1;37m└───────────────────────────────────────────────────────────────────┘\033[0m\n\n"
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
|
|
||||||
if cat /sys/class/drm/*/status | grep -qw connected; then
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
SOURCE_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
SOURCE_DIR="$(dirname $(realpath "${BASH_SOURCE[0]}"))"
|
||||||
|
|
||||||
if [ "$UID" -ne 0 ]; then
|
if [ "$UID" -ne 0 ]; then
|
||||||
>&2 echo 'Must be run as root'
|
>&2 echo 'Must be run as root'
|
||||||
@@ -10,24 +10,24 @@ fi
|
|||||||
POSITIONAL_ARGS=()
|
POSITIONAL_ARGS=()
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
--no-sync)
|
--no-sync)
|
||||||
NO_SYNC=1
|
NO_SYNC=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--create)
|
--create)
|
||||||
ONLY_CREATE=1
|
ONLY_CREATE=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
-*|--*)
|
-*|--*)
|
||||||
echo "Unknown option $1"
|
echo "Unknown option $1"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
POSITIONAL_ARGS+=("$1") # save positional arg
|
POSITIONAL_ARGS+=("$1") # save positional arg
|
||||||
shift # past argument
|
shift # past argument
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
|
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
|
||||||
@@ -35,7 +35,7 @@ set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
|
|||||||
if [ -z "$NO_SYNC" ]; then
|
if [ -z "$NO_SYNC" ]; then
|
||||||
echo 'Syncing...'
|
echo 'Syncing...'
|
||||||
umount -R /media/startos/next 2> /dev/null
|
umount -R /media/startos/next 2> /dev/null
|
||||||
umount -R /media/startos/upper 2> /dev/null
|
umount /media/startos/upper 2> /dev/null
|
||||||
rm -rf /media/startos/upper /media/startos/next
|
rm -rf /media/startos/upper /media/startos/next
|
||||||
mkdir /media/startos/upper
|
mkdir /media/startos/upper
|
||||||
mount -t tmpfs tmpfs /media/startos/upper
|
mount -t tmpfs tmpfs /media/startos/upper
|
||||||
@@ -43,8 +43,6 @@ if [ -z "$NO_SYNC" ]; then
|
|||||||
mount -t overlay \
|
mount -t overlay \
|
||||||
-olowerdir=/media/startos/current,upperdir=/media/startos/upper/data,workdir=/media/startos/upper/work \
|
-olowerdir=/media/startos/current,upperdir=/media/startos/upper/data,workdir=/media/startos/upper/work \
|
||||||
overlay /media/startos/next
|
overlay /media/startos/next
|
||||||
mkdir -p /media/startos/next/media/startos/root
|
|
||||||
mount --bind /media/startos/root /media/startos/next/media/startos/root
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$ONLY_CREATE" ]; then
|
if [ -n "$ONLY_CREATE" ]; then
|
||||||
@@ -56,12 +54,18 @@ mkdir -p /media/startos/next/dev
|
|||||||
mkdir -p /media/startos/next/sys
|
mkdir -p /media/startos/next/sys
|
||||||
mkdir -p /media/startos/next/proc
|
mkdir -p /media/startos/next/proc
|
||||||
mkdir -p /media/startos/next/boot
|
mkdir -p /media/startos/next/boot
|
||||||
|
mkdir -p /media/startos/next/media/startos/root
|
||||||
mount --bind /run /media/startos/next/run
|
mount --bind /run /media/startos/next/run
|
||||||
mount --bind /tmp /media/startos/next/tmp
|
mount --bind /tmp /media/startos/next/tmp
|
||||||
mount --bind /dev /media/startos/next/dev
|
mount --bind /dev /media/startos/next/dev
|
||||||
mount --bind /sys /media/startos/next/sys
|
mount --bind /sys /media/startos/next/sys
|
||||||
mount --bind /proc /media/startos/next/proc
|
mount --bind /proc /media/startos/next/proc
|
||||||
mount --bind /boot /media/startos/next/boot
|
mount --bind /boot /media/startos/next/boot
|
||||||
|
mount --bind /media/startos/root /media/startos/next/media/startos/root
|
||||||
|
|
||||||
|
if mountpoint /sys/firmware/efi/efivars 2>&1 > /dev/null; then
|
||||||
|
mount --bind /sys/firmware/efi/efivars /media/startos/next/sys/firmware/efi/efivars
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "$*" ]; then
|
if [ -z "$*" ]; then
|
||||||
chroot /media/startos/next
|
chroot /media/startos/next
|
||||||
@@ -71,6 +75,10 @@ else
|
|||||||
CHROOT_RES=$?
|
CHROOT_RES=$?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if mountpoint /media/startos/next/sys/firmware/efi/efivars 2>&1 > /dev/null; then
|
||||||
|
umount /media/startos/next/sys/firmware/efi/efivars
|
||||||
|
fi
|
||||||
|
|
||||||
umount /media/startos/next/run
|
umount /media/startos/next/run
|
||||||
umount /media/startos/next/tmp
|
umount /media/startos/next/tmp
|
||||||
umount /media/startos/next/dev
|
umount /media/startos/next/dev
|
||||||
@@ -87,11 +95,12 @@ if [ "$CHROOT_RES" -eq 0 ]; then
|
|||||||
|
|
||||||
echo 'Upgrading...'
|
echo 'Upgrading...'
|
||||||
|
|
||||||
|
rm -f /media/startos/images/next.squashfs
|
||||||
if ! time mksquashfs /media/startos/next /media/startos/images/next.squashfs -b 4096 -comp gzip; then
|
if ! time mksquashfs /media/startos/next /media/startos/images/next.squashfs -b 4096 -comp gzip; then
|
||||||
umount -R /media/startos/next
|
umount -l /media/startos/next
|
||||||
umount -R /media/startos/upper
|
umount -l /media/startos/upper
|
||||||
rm -rf /media/startos/upper /media/startos/next
|
rm -rf /media/startos/upper /media/startos/next
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
hash=$(b3sum /media/startos/images/next.squashfs | head -c 32)
|
hash=$(b3sum /media/startos/images/next.squashfs | head -c 32)
|
||||||
mv /media/startos/images/next.squashfs /media/startos/images/${hash}.rootfs
|
mv /media/startos/images/next.squashfs /media/startos/images/${hash}.rootfs
|
||||||
@@ -102,6 +111,6 @@ if [ "$CHROOT_RES" -eq 0 ]; then
|
|||||||
reboot
|
reboot
|
||||||
fi
|
fi
|
||||||
|
|
||||||
umount -R /media/startos/next
|
umount /media/startos/next
|
||||||
umount -R /media/startos/upper
|
umount /media/startos/upper
|
||||||
rm -rf /media/startos/upper /media/startos/next
|
rm -rf /media/startos/upper /media/startos/next
|
||||||
@@ -27,7 +27,6 @@ user_pref("browser.crashReports.unsubmittedCheck.autoSubmit2", false);
|
|||||||
user_pref("browser.newtabpage.activity-stream.feeds.asrouterfeed", false);
|
user_pref("browser.newtabpage.activity-stream.feeds.asrouterfeed", false);
|
||||||
user_pref("browser.newtabpage.activity-stream.feeds.topsites", false);
|
user_pref("browser.newtabpage.activity-stream.feeds.topsites", false);
|
||||||
user_pref("browser.newtabpage.activity-stream.showSponsoredTopSites", false);
|
user_pref("browser.newtabpage.activity-stream.showSponsoredTopSites", false);
|
||||||
user_pref("browser.onboarding.enabled", false);
|
|
||||||
user_pref("browser.ping-centre.telemetry", false);
|
user_pref("browser.ping-centre.telemetry", false);
|
||||||
user_pref("browser.pocket.enabled", false);
|
user_pref("browser.pocket.enabled", false);
|
||||||
user_pref("browser.safebrowsing.blockedURIs.enabled", false);
|
user_pref("browser.safebrowsing.blockedURIs.enabled", false);
|
||||||
@@ -43,7 +42,7 @@ user_pref("browser.startup.homepage_override.mstone", "ignore");
|
|||||||
user_pref("browser.theme.content-theme", 0);
|
user_pref("browser.theme.content-theme", 0);
|
||||||
user_pref("browser.theme.toolbar-theme", 0);
|
user_pref("browser.theme.toolbar-theme", 0);
|
||||||
user_pref("browser.urlbar.groupLabels.enabled", false);
|
user_pref("browser.urlbar.groupLabels.enabled", false);
|
||||||
user_pref("browser.urlbar.suggest.searches" false);
|
user_pref("browser.urlbar.suggest.searches", false);
|
||||||
user_pref("datareporting.policy.firstRunURL", "");
|
user_pref("datareporting.policy.firstRunURL", "");
|
||||||
user_pref("datareporting.healthreport.service.enabled", false);
|
user_pref("datareporting.healthreport.service.enabled", false);
|
||||||
user_pref("datareporting.healthreport.uploadEnabled", false);
|
user_pref("datareporting.healthreport.uploadEnabled", false);
|
||||||
@@ -52,10 +51,9 @@ user_pref("dom.securecontext.allowlist_onions", true);
|
|||||||
user_pref("dom.securecontext.whitelist_onions", true);
|
user_pref("dom.securecontext.whitelist_onions", true);
|
||||||
user_pref("experiments.enabled", false);
|
user_pref("experiments.enabled", false);
|
||||||
user_pref("experiments.activeExperiment", false);
|
user_pref("experiments.activeExperiment", false);
|
||||||
user_pref("experiments.supported", false);
|
|
||||||
user_pref("extensions.activeThemeID", "firefox-compact-dark@mozilla.org");
|
user_pref("extensions.activeThemeID", "firefox-compact-dark@mozilla.org");
|
||||||
user_pref("extensions.blocklist.enabled", false);
|
user_pref("extensions.blocklist.enabled", false);
|
||||||
user_pref("extensions.getAddons.cache.enabled", false);
|
user_pref("extensions.htmlaboutaddons.recommendations.enabled", false);
|
||||||
user_pref("extensions.pocket.enabled", false);
|
user_pref("extensions.pocket.enabled", false);
|
||||||
user_pref("extensions.update.enabled", false);
|
user_pref("extensions.update.enabled", false);
|
||||||
user_pref("extensions.shield-recipe-client.enabled", false);
|
user_pref("extensions.shield-recipe-client.enabled", false);
|
||||||
@@ -66,9 +64,15 @@ user_pref("messaging-system.rsexperimentloader.enabled", false);
|
|||||||
user_pref("network.allow-experiments", false);
|
user_pref("network.allow-experiments", false);
|
||||||
user_pref("network.captive-portal-service.enabled", false);
|
user_pref("network.captive-portal-service.enabled", false);
|
||||||
user_pref("network.connectivity-service.enabled", false);
|
user_pref("network.connectivity-service.enabled", false);
|
||||||
user_pref("network.proxy.autoconfig_url", "file:///usr/lib/startos/proxy.pac");
|
user_pref("network.proxy.socks", "10.0.3.1");
|
||||||
|
user_pref("network.proxy.socks_port", 9050);
|
||||||
|
user_pref("network.proxy.socks_version", 5);
|
||||||
user_pref("network.proxy.socks_remote_dns", true);
|
user_pref("network.proxy.socks_remote_dns", true);
|
||||||
user_pref("network.proxy.type", 2);
|
user_pref("network.proxy.type", 1);
|
||||||
|
user_pref("privacy.resistFingerprinting", true);
|
||||||
|
//Enable letterboxing if we want the window size sent to the server to snap to common resolutions:
|
||||||
|
//user_pref("privacy.resistFingerprinting.letterboxing", true);
|
||||||
|
user_pref("privacy.trackingprotection.enabled", true);
|
||||||
user_pref("signon.rememberSignons", false);
|
user_pref("signon.rememberSignons", false);
|
||||||
user_pref("toolkit.telemetry.archive.enabled", false);
|
user_pref("toolkit.telemetry.archive.enabled", false);
|
||||||
user_pref("toolkit.telemetry.bhrPing.enabled", false);
|
user_pref("toolkit.telemetry.bhrPing.enabled", false);
|
||||||
@@ -81,6 +85,17 @@ user_pref("toolkit.telemetry.shutdownPingSender.enabled", false);
|
|||||||
user_pref("toolkit.telemetry.unified", false);
|
user_pref("toolkit.telemetry.unified", false);
|
||||||
user_pref("toolkit.telemetry.updatePing.enabled", false);
|
user_pref("toolkit.telemetry.updatePing.enabled", false);
|
||||||
user_pref("toolkit.telemetry.cachedClientID", "");
|
user_pref("toolkit.telemetry.cachedClientID", "");
|
||||||
|
//Blocking automatic Mozilla CDN server requests
|
||||||
|
user_pref("extensions.getAddons.showPane", false);
|
||||||
|
user_pref("extensions.getAddons.cache.enabled", false);
|
||||||
|
//user_pref("services.settings.server", ""); // Remote settings server (HSTS preload updates and Cerfiticate Revocation Lists are fetched)
|
||||||
|
user_pref("browser.aboutHomeSnippets.updateUrl", "");
|
||||||
|
user_pref("browser.newtabpage.activity-stream.feeds.snippets", false);
|
||||||
|
user_pref("browser.newtabpage.activity-stream.feeds.section.topstories", false);
|
||||||
|
user_pref("browser.newtabpage.activity-stream.feeds.system.topstories", false);
|
||||||
|
user_pref("browser.newtabpage.activity-stream.feeds.discoverystreamfeed", false);
|
||||||
|
user_pref("browser.safebrowsing.provider.mozilla.updateURL", "");
|
||||||
|
user_pref("browser.safebrowsing.provider.mozilla.gethashURL", "");
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
ln -sf /usr/lib/$(uname -m)-linux-gnu/pkcs11/p11-kit-trust.so /usr/lib/firefox-esr/libnssckbi.so
|
ln -sf /usr/lib/$(uname -m)-linux-gnu/pkcs11/p11-kit-trust.so /usr/lib/firefox-esr/libnssckbi.so
|
||||||
@@ -91,15 +106,6 @@ cat > /home/kiosk/kiosk.sh << 'EOF'
|
|||||||
while ! curl "http://localhost" > /dev/null; do
|
while ! curl "http://localhost" > /dev/null; do
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
while ! /usr/lib/startos/scripts/check-monitor; do
|
|
||||||
sleep 15
|
|
||||||
done
|
|
||||||
(
|
|
||||||
while /usr/lib/startos/scripts/check-monitor; do
|
|
||||||
sleep 15
|
|
||||||
done
|
|
||||||
killall firefox-esr
|
|
||||||
) &
|
|
||||||
matchbox-window-manager -use_titlebar no &
|
matchbox-window-manager -use_titlebar no &
|
||||||
cp -r /home/kiosk/fx-profile /home/kiosk/fx-profile-tmp
|
cp -r /home/kiosk/fx-profile /home/kiosk/fx-profile-tmp
|
||||||
firefox-esr http://localhost --profile /home/kiosk/fx-profile-tmp
|
firefox-esr http://localhost --profile /home/kiosk/fx-profile-tmp
|
||||||
|
|||||||
72
build/lib/scripts/forward-port
Executable file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ -z "$sip" ] || [ -z "$dip" ] || [ -z "$dprefix" ] || [ -z "$sport" ] || [ -z "$dport" ]; then
|
||||||
|
>&2 echo 'missing required env var'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NAME="F$(echo "$sip:$sport -> $dip/$dprefix:$dport ${src_subnet:-any}" | sha256sum | head -c 15)"
|
||||||
|
|
||||||
|
for kind in INPUT FORWARD ACCEPT; do
|
||||||
|
if ! iptables -C $kind -j "${NAME}_${kind}" 2> /dev/null; then
|
||||||
|
iptables -N "${NAME}_${kind}" 2> /dev/null
|
||||||
|
iptables -A $kind -j "${NAME}_${kind}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for kind in PREROUTING OUTPUT POSTROUTING; do
|
||||||
|
if ! iptables -t nat -C $kind -j "${NAME}_${kind}" 2> /dev/null; then
|
||||||
|
iptables -t nat -N "${NAME}_${kind}" 2> /dev/null
|
||||||
|
iptables -t nat -A $kind -j "${NAME}_${kind}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
err=0
|
||||||
|
trap 'err=1' ERR
|
||||||
|
|
||||||
|
for kind in INPUT FORWARD ACCEPT; do
|
||||||
|
iptables -F "${NAME}_${kind}" 2> /dev/null
|
||||||
|
done
|
||||||
|
for kind in PREROUTING OUTPUT POSTROUTING; do
|
||||||
|
iptables -t nat -F "${NAME}_${kind}" 2> /dev/null
|
||||||
|
done
|
||||||
|
if [ "$UNDO" = 1 ]; then
|
||||||
|
conntrack -D -p tcp -d $sip --dport $sport || true # conntrack returns exit 1 if no connections are active
|
||||||
|
conntrack -D -p udp -d $sip --dport $sport || true # conntrack returns exit 1 if no connections are active
|
||||||
|
exit $err
|
||||||
|
fi
|
||||||
|
|
||||||
|
# DNAT: rewrite destination for incoming packets (external traffic)
|
||||||
|
# When src_subnet is set, only forward traffic from that subnet (private forwards)
|
||||||
|
if [ -n "$src_subnet" ]; then
|
||||||
|
iptables -t nat -A ${NAME}_PREROUTING -s "$src_subnet" -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
||||||
|
iptables -t nat -A ${NAME}_PREROUTING -s "$src_subnet" -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
||||||
|
# Also allow containers on the bridge subnet to reach this forward
|
||||||
|
if [ -n "$bridge_subnet" ]; then
|
||||||
|
iptables -t nat -A ${NAME}_PREROUTING -s "$bridge_subnet" -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
||||||
|
iptables -t nat -A ${NAME}_PREROUTING -s "$bridge_subnet" -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
||||||
|
iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# DNAT: rewrite destination for locally-originated packets (hairpin from host itself)
|
||||||
|
iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
||||||
|
iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
||||||
|
|
||||||
|
# Allow new connections to be forwarded to the destination
|
||||||
|
iptables -A ${NAME}_FORWARD -d $dip -p tcp --dport $dport -m state --state NEW -j ACCEPT
|
||||||
|
iptables -A ${NAME}_FORWARD -d $dip -p udp --dport $dport -m state --state NEW -j ACCEPT
|
||||||
|
|
||||||
|
# NAT hairpin: masquerade traffic from the bridge subnet or host to the DNAT
|
||||||
|
# target, so replies route back through the host for proper NAT reversal.
|
||||||
|
# Container-to-container hairpin (source is on the bridge subnet)
|
||||||
|
if [ -n "$bridge_subnet" ]; then
|
||||||
|
iptables -t nat -A ${NAME}_POSTROUTING -s "$bridge_subnet" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE
|
||||||
|
iptables -t nat -A ${NAME}_POSTROUTING -s "$bridge_subnet" -d "$dip" -p udp --dport "$dport" -j MASQUERADE
|
||||||
|
fi
|
||||||
|
# Host-to-container hairpin (host connects to its own gateway IP, source is sip)
|
||||||
|
iptables -t nat -A ${NAME}_POSTROUTING -s "$sip" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE
|
||||||
|
iptables -t nat -A ${NAME}_POSTROUTING -s "$sip" -d "$dip" -p udp --dport "$dport" -j MASQUERADE
|
||||||
|
|
||||||
|
exit $err
|
||||||
20
build/lib/scripts/install-equivs
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
export DEBCONF_NONINTERACTIVE_SEEN=true
|
||||||
|
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
cd $TMP_DIR
|
||||||
|
|
||||||
|
cat > control.equivs
|
||||||
|
equivs-build control.equivs
|
||||||
|
apt-get install -y ./*.deb < /dev/null
|
||||||
|
)
|
||||||
|
|
||||||
|
rm -rf $TMP_DIR
|
||||||
|
|
||||||
|
echo Install complete. >&2
|
||||||
|
exit 0
|
||||||
35
build/lib/scripts/prune-boot
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$UID" -ne 0 ]; then
|
||||||
|
>&2 echo 'Must be run as root'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the current kernel version
|
||||||
|
current_kernel=$(uname -r)
|
||||||
|
|
||||||
|
echo "Current kernel: $current_kernel"
|
||||||
|
echo "Searching for old kernel files in /boot..."
|
||||||
|
|
||||||
|
# Extract base kernel version (without possible suffixes)
|
||||||
|
current_base=$(echo "$current_kernel" | sed 's/-.*//')
|
||||||
|
|
||||||
|
cd /boot || { echo "/boot directory not found!"; exit 1; }
|
||||||
|
|
||||||
|
for file in vmlinuz-* initrd.img-* System.map-* config-*; do
|
||||||
|
# Extract version from filename
|
||||||
|
version=$(echo "$file" | sed -E 's/^[^0-9]*([0-9][^ ]*).*/\1/')
|
||||||
|
# Skip if file matches current kernel version
|
||||||
|
if [[ "$file" == *"$current_kernel"* ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Compare versions, delete if less than current
|
||||||
|
if dpkg --compare-versions "$version" lt "$current_kernel"; then
|
||||||
|
echo "Deleting $file (version $version is older than $current_kernel)"
|
||||||
|
sudo rm -f "$file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Old kernel files deleted."
|
||||||
@@ -29,10 +29,13 @@ if [ -z "$needed" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
MARGIN=${MARGIN:-1073741824}
|
||||||
|
target=$((needed + MARGIN))
|
||||||
|
|
||||||
if [ -h /media/startos/config/current.rootfs ] && [ -e /media/startos/config/current.rootfs ]; then
|
if [ -h /media/startos/config/current.rootfs ] && [ -e /media/startos/config/current.rootfs ]; then
|
||||||
echo 'Pruning...'
|
echo 'Pruning...'
|
||||||
current="$(readlink -f /media/startos/config/current.rootfs)"
|
current="$(readlink -f /media/startos/config/current.rootfs)"
|
||||||
while [[ "$(df -B1 --output=avail --sync /media/startos/images | tail -n1)" -lt "$needed" ]]; do
|
while [[ "$(df -B1 --output=avail --sync /media/startos/images | tail -n1)" -lt "$target" ]]; do
|
||||||
to_prune="$(ls -t1 /media/startos/images/*.rootfs /media/startos/images/*.squashfs 2> /dev/null | grep -v "$current" | tail -n1)"
|
to_prune="$(ls -t1 /media/startos/images/*.rootfs /media/startos/images/*.squashfs 2> /dev/null | grep -v "$current" | tail -n1)"
|
||||||
if [ -e "$to_prune" ]; then
|
if [ -e "$to_prune" ]; then
|
||||||
echo " Pruning $to_prune"
|
echo " Pruning $to_prune"
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ local_mount_root()
|
|||||||
if [ -d "$image" ]; then
|
if [ -d "$image" ]; then
|
||||||
mount -r --bind $image /lower
|
mount -r --bind $image /lower
|
||||||
elif [ -f "$image" ]; then
|
elif [ -f "$image" ]; then
|
||||||
|
modprobe loop
|
||||||
modprobe squashfs
|
modprobe squashfs
|
||||||
mount -r $image /lower
|
mount -r $image /lower
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,36 +1,64 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
fail=$(printf " [\033[31m fail \033[0m]")
|
# --- Config ---
|
||||||
pass=$(printf " [\033[32m pass \033[0m]")
|
# Colors (using printf to ensure compatibility)
|
||||||
|
GRAY=$(printf '\033[90m')
|
||||||
|
GREEN=$(printf '\033[32m')
|
||||||
|
RED=$(printf '\033[31m')
|
||||||
|
NC=$(printf '\033[0m') # No Color
|
||||||
|
|
||||||
|
# Proxies to test
|
||||||
|
proxies=(
|
||||||
|
"Host Tor|127.0.1.1:9050"
|
||||||
|
"Startd Tor|10.0.3.1:9050"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Default URLs
|
||||||
onion_list=(
|
onion_list=(
|
||||||
|
"The Tor Project|http://2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion"
|
||||||
"Start9|http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion"
|
"Start9|http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion"
|
||||||
"Mempool|http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion"
|
"Mempool|http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion"
|
||||||
"DuckDuckGo|https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion"
|
"DuckDuckGo|https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion"
|
||||||
"Brave Search|https://search.brave4u7jddbv7cyviptqjc7jusxh72uik7zt6adtckl5f4nwy2v72qd.onion"
|
"Brave Search|https://search.brave4u7jddbv7cyviptqjc7jusxh72uik7zt6adtckl5f4nwy2v72qd.onion"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if ~/.startos/tor-check.list exists and read its contents if available
|
# Load custom list
|
||||||
if [ -f ~/.startos/tor-check.list ]; then
|
[ -f ~/.startos/tor-check.list ] && readarray -t custom_list < <(grep -v '^#' ~/.startos/tor-check.list) && onion_list+=("${custom_list[@]}")
|
||||||
while IFS= read -r line; do
|
|
||||||
# Check if the line starts with a #
|
# --- Functions ---
|
||||||
if [[ ! "$line" =~ ^# ]]; then
|
print_line() { printf "${GRAY}────────────────────────────────────────${NC}\n"; }
|
||||||
onion_list+=("$line")
|
|
||||||
|
# --- Main ---
|
||||||
|
echo "Testing Onion Connections..."
|
||||||
|
|
||||||
|
for proxy_info in "${proxies[@]}"; do
|
||||||
|
proxy_name="${proxy_info%%|*}"
|
||||||
|
proxy_addr="${proxy_info#*|}"
|
||||||
|
|
||||||
|
print_line
|
||||||
|
printf "${GRAY}Proxy: %s (%s)${NC}\n" "$proxy_name" "$proxy_addr"
|
||||||
|
|
||||||
|
for data in "${onion_list[@]}"; do
|
||||||
|
name="${data%%|*}"
|
||||||
|
url="${data#*|}"
|
||||||
|
|
||||||
|
# Capture verbose output + http code.
|
||||||
|
# --no-progress-meter: Suppresses the "0 0 0" stats but keeps -v output
|
||||||
|
output=$(curl -v --no-progress-meter --max-time 15 --socks5-hostname "$proxy_addr" "$url" 2>&1)
|
||||||
|
exit_code=$?
|
||||||
|
|
||||||
|
if [ $exit_code -eq 0 ]; then
|
||||||
|
printf " ${GREEN}[pass]${NC} %s (%s)\n" "$name" "$url"
|
||||||
|
else
|
||||||
|
printf " ${RED}[fail]${NC} %s (%s)\n" "$name" "$url"
|
||||||
|
printf " ${RED}↳ Curl Error %s${NC}\n" "$exit_code"
|
||||||
|
|
||||||
|
# Print the last 4 lines of verbose log to show the specific handshake error
|
||||||
|
# We look for lines starting with '*' or '>' or '<' to filter out junk if any remains
|
||||||
|
echo "$output" | tail -n 4 | sed "s/^/ ${GRAY}/"
|
||||||
fi
|
fi
|
||||||
done < ~/.startos/tor-check.list
|
done
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Testing connection to Onion Pages ..."
|
|
||||||
|
|
||||||
for data in "${onion_list[@]}"; do
|
|
||||||
name="${data%%|*}"
|
|
||||||
url="${data#*|}"
|
|
||||||
if curl --socks5-hostname localhost:9050 "$url" > /dev/null 2>&1; then
|
|
||||||
echo " ${pass}: $name ($url) "
|
|
||||||
else
|
|
||||||
echo " ${fail}: $name ($url) "
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
print_line
|
||||||
echo
|
# Reset color just in case
|
||||||
echo "Done."
|
printf "${NC}"
|
||||||
|
|||||||
82
build/lib/scripts/upgrade
Executable file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SOURCE_DIR="$(dirname $(realpath "${BASH_SOURCE[0]}"))"
|
||||||
|
|
||||||
|
if [ "$UID" -ne 0 ]; then
|
||||||
|
>&2 echo 'Must be run as root'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [ -f "$1" ]; then
|
||||||
|
>&2 echo "usage: $0 <SQUASHFS>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo 'Upgrading...'
|
||||||
|
|
||||||
|
hash=$(b3sum $1 | head -c 32)
|
||||||
|
if [ -n "$2" ] && [ "$hash" != "$CHECKSUM" ]; then
|
||||||
|
>&2 echo 'Checksum mismatch'
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
unsquashfs -f -d / $1 boot
|
||||||
|
|
||||||
|
umount -R /media/startos/next 2> /dev/null || true
|
||||||
|
umount /media/startos/upper 2> /dev/null || true
|
||||||
|
umount /media/startos/lower 2> /dev/null || true
|
||||||
|
|
||||||
|
mkdir -p /media/startos/upper
|
||||||
|
mount -t tmpfs tmpfs /media/startos/upper
|
||||||
|
mkdir -p /media/startos/lower /media/startos/upper/data /media/startos/upper/work /media/startos/next
|
||||||
|
mount $1 /media/startos/lower
|
||||||
|
mount -t overlay \
|
||||||
|
-olowerdir=/media/startos/lower,upperdir=/media/startos/upper/data,workdir=/media/startos/upper/work \
|
||||||
|
overlay /media/startos/next
|
||||||
|
|
||||||
|
mkdir -p /media/startos/next/run
|
||||||
|
mkdir -p /media/startos/next/dev
|
||||||
|
mkdir -p /media/startos/next/sys
|
||||||
|
mkdir -p /media/startos/next/proc
|
||||||
|
mkdir -p /media/startos/next/boot
|
||||||
|
mkdir -p /media/startos/next/media/startos/root
|
||||||
|
mount --bind /run /media/startos/next/run
|
||||||
|
mount --bind /tmp /media/startos/next/tmp
|
||||||
|
mount --bind /dev /media/startos/next/dev
|
||||||
|
mount --bind /sys /media/startos/next/sys
|
||||||
|
mount --bind /proc /media/startos/next/proc
|
||||||
|
mount --bind /boot /media/startos/next/boot
|
||||||
|
mount --bind /media/startos/root /media/startos/next/media/startos/root
|
||||||
|
|
||||||
|
if mountpoint /boot/efi 2>&1 > /dev/null; then
|
||||||
|
mkdir -p /media/startos/next/boot/efi
|
||||||
|
mount --bind /boot/efi /media/startos/next/boot/efi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if mountpoint /sys/firmware/efi/efivars 2>&1 > /dev/null; then
|
||||||
|
mount --bind /sys/firmware/efi/efivars /media/startos/next/sys/firmware/efi/efivars
|
||||||
|
fi
|
||||||
|
|
||||||
|
chroot /media/startos/next bash -e << "EOF"
|
||||||
|
|
||||||
|
if [ -f /boot/grub/grub.cfg ]; then
|
||||||
|
grub-install /dev/$(eval $(lsblk -o MOUNTPOINT,PKNAME -P | grep 'MOUNTPOINT="/media/startos/root"') && echo $PKNAME)
|
||||||
|
update-grub
|
||||||
|
fi
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sync
|
||||||
|
|
||||||
|
umount -Rl /media/startos/next
|
||||||
|
umount /media/startos/upper
|
||||||
|
umount /media/startos/lower
|
||||||
|
|
||||||
|
mv $1 /media/startos/images/${hash}.rootfs
|
||||||
|
ln -rsf /media/startos/images/${hash}.rootfs /media/startos/config/current.rootfs
|
||||||
|
|
||||||
|
sync
|
||||||
|
|
||||||
|
echo 'System upgrade complete. Reboot to apply changes...'
|
||||||
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
25
build/os-compat/buildenv.Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
FROM debian:trixie
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
gpg \
|
||||||
|
build-essential \
|
||||||
|
sed \
|
||||||
|
grep \
|
||||||
|
gawk \
|
||||||
|
jq \
|
||||||
|
gzip \
|
||||||
|
brotli \
|
||||||
|
squashfs-tools \
|
||||||
|
git \
|
||||||
|
rsync \
|
||||||
|
b3sum \
|
||||||
|
sudo \
|
||||||
|
nodejs
|
||||||
|
|
||||||
|
RUN git config --global --add safe.directory /root/start-os
|
||||||
|
|
||||||
|
RUN mkdir -p /root/start-os
|
||||||
|
WORKDIR /root/start-os
|
||||||
30
build/os-compat/run-compat.sh
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
pwd=$(pwd)
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
rel_pwd="${pwd#"$(pwd)"}"
|
||||||
|
|
||||||
|
COMPAT_ARCH=$(uname -m)
|
||||||
|
|
||||||
|
platform=linux/$COMPAT_ARCH
|
||||||
|
|
||||||
|
case $COMPAT_ARCH in
|
||||||
|
x86_64)
|
||||||
|
platform=linux/amd64;;
|
||||||
|
aarch64)
|
||||||
|
platform=linux/arm64;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$FORCE_COMPAT" = 1 ] || ( [ "$REQUIRES" = "linux" ] && [ "$(uname -s)" != "Linux" ] ) || ( [ "$REQUIRES" = "debian" ] && ! which dpkg > /dev/null ); then
|
||||||
|
if tty -s; then
|
||||||
|
USE_TTY="-it"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run $USE_TTY --platform=$platform -eARCH -eENVIRONMENT -ePLATFORM -eGIT_BRANCH_AS_HASH -ePROJECT -eDEPENDS -eCONFLICTS -w "/root/start-os${rel_pwd}" --rm -v "$(pwd):/root/start-os" start9/build-env $@
|
||||||
|
else
|
||||||
|
exec $@
|
||||||
|
fi
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
function partition_for () {
|
|
||||||
if [[ "$1" =~ [0-9]+$ ]]; then
|
|
||||||
echo "$1p$2"
|
|
||||||
else
|
|
||||||
echo "$1$2"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
VERSION=$(cat VERSION.txt)
|
|
||||||
ENVIRONMENT=$(cat ENVIRONMENT.txt)
|
|
||||||
GIT_HASH=$(cat GIT_HASH.txt | head -c 7)
|
|
||||||
DATE=$(date +%Y%m%d)
|
|
||||||
|
|
||||||
ROOT_PART_END=7217792
|
|
||||||
|
|
||||||
VERSION_FULL="$VERSION-$GIT_HASH"
|
|
||||||
|
|
||||||
if [ -n "$ENVIRONMENT" ]; then
|
|
||||||
VERSION_FULL="$VERSION_FULL~$ENVIRONMENT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
TARGET_NAME=startos-${VERSION_FULL}-${DATE}_raspberrypi.img
|
|
||||||
TARGET_SIZE=$[($ROOT_PART_END+1)*512]
|
|
||||||
|
|
||||||
rm -f $TARGET_NAME
|
|
||||||
truncate -s $TARGET_SIZE $TARGET_NAME
|
|
||||||
(
|
|
||||||
echo o
|
|
||||||
echo x
|
|
||||||
echo i
|
|
||||||
echo "0xcb15ae4d"
|
|
||||||
echo r
|
|
||||||
echo n
|
|
||||||
echo p
|
|
||||||
echo 1
|
|
||||||
echo 2048
|
|
||||||
echo 526335
|
|
||||||
echo t
|
|
||||||
echo c
|
|
||||||
echo n
|
|
||||||
echo p
|
|
||||||
echo 2
|
|
||||||
echo 526336
|
|
||||||
echo $ROOT_PART_END
|
|
||||||
echo a
|
|
||||||
echo 1
|
|
||||||
echo w
|
|
||||||
) | fdisk $TARGET_NAME
|
|
||||||
OUTPUT_DEVICE=$(sudo losetup --show -fP $TARGET_NAME)
|
|
||||||
sudo mkfs.ext4 `partition_for ${OUTPUT_DEVICE} 2`
|
|
||||||
sudo mkfs.vfat `partition_for ${OUTPUT_DEVICE} 1`
|
|
||||||
|
|
||||||
TMPDIR=$(mktemp -d)
|
|
||||||
|
|
||||||
sudo mount `partition_for ${OUTPUT_DEVICE} 2` $TMPDIR
|
|
||||||
sudo mkdir $TMPDIR/boot
|
|
||||||
sudo mount `partition_for ${OUTPUT_DEVICE} 1` $TMPDIR/boot
|
|
||||||
sudo unsquashfs -f -d $TMPDIR startos.raspberrypi.squashfs
|
|
||||||
REAL_GIT_HASH=$(cat $TMPDIR/usr/lib/startos/GIT_HASH.txt)
|
|
||||||
REAL_VERSION=$(cat $TMPDIR/usr/lib/startos/VERSION.txt)
|
|
||||||
REAL_ENVIRONMENT=$(cat $TMPDIR/usr/lib/startos/ENVIRONMENT.txt)
|
|
||||||
sudo sed -i 's| boot=startos| init=/usr/lib/startos/scripts/init_resize\.sh|' $TMPDIR/boot/cmdline.txt
|
|
||||||
sudo cp ./build/raspberrypi/fstab $TMPDIR/etc/
|
|
||||||
sudo cp ./build/raspberrypi/init_resize.sh $TMPDIR/usr/lib/startos/scripts/init_resize.sh
|
|
||||||
sudo umount $TMPDIR/boot
|
|
||||||
sudo umount $TMPDIR
|
|
||||||
sudo losetup -d $OUTPUT_DEVICE
|
|
||||||
|
|
||||||
if [ "$ALLOW_VERSION_MISMATCH" != 1 ]; then
|
|
||||||
if [ "$(cat GIT_HASH.txt)" != "$REAL_GIT_HASH" ]; then
|
|
||||||
>&2 echo "startos.raspberrypi.squashfs GIT_HASH.txt mismatch"
|
|
||||||
>&2 echo "expected $REAL_GIT_HASH (dpkg) found $(cat GIT_HASH.txt) (repo)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ "$(cat VERSION.txt)" != "$REAL_VERSION" ]; then
|
|
||||||
>&2 echo "startos.raspberrypi.squashfs VERSION.txt mismatch"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ "$(cat ENVIRONMENT.txt)" != "$REAL_ENVIRONMENT" ]; then
|
|
||||||
>&2 echo "startos.raspberrypi.squashfs ENVIRONMENT.txt mismatch"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
142
build/upload-ota.sh
Executable file
@@ -0,0 +1,142 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
>&2 echo '$VERSION required'
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$SKIP_DL" != "1" ]; then
|
||||||
|
if [ "$SKIP_CLEAN" != "1" ]; then
|
||||||
|
rm -rf ~/Downloads/v$VERSION
|
||||||
|
mkdir ~/Downloads/v$VERSION
|
||||||
|
cd ~/Downloads/v$VERSION
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$RUN_ID" ]; then
|
||||||
|
for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do
|
||||||
|
while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.squashfs -D $(pwd); do sleep 1; done
|
||||||
|
done
|
||||||
|
for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do
|
||||||
|
while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.iso -D $(pwd); do sleep 1; done
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$ST_RUN_ID" ]; then
|
||||||
|
for arch in aarch64 riscv64 x86_64; do
|
||||||
|
while ! gh run download -R Start9Labs/start-os $ST_RUN_ID -n start-tunnel_$arch.deb -D $(pwd); do sleep 1; done
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$CLI_RUN_ID" ]; then
|
||||||
|
for arch in aarch64 riscv64 x86_64; do
|
||||||
|
for os in linux macos; do
|
||||||
|
pair=${arch}-${os}
|
||||||
|
if [ "${pair}" = "riscv64-linux" ]; then
|
||||||
|
target=riscv64gc-unknown-linux-musl
|
||||||
|
elif [ "${pair}" = "riscv64-macos" ]; then
|
||||||
|
continue
|
||||||
|
elif [ "${os}" = "linux" ]; then
|
||||||
|
target="${arch}-unknown-linux-musl"
|
||||||
|
elif [ "${os}" = "macos" ]; then
|
||||||
|
target="${arch}-apple-darwin"
|
||||||
|
fi
|
||||||
|
while ! gh run download -R Start9Labs/start-os $CLI_RUN_ID -n start-cli_$target -D $(pwd); do sleep 1; done
|
||||||
|
mv start-cli "start-cli_${pair}"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
cd ~/Downloads/v$VERSION
|
||||||
|
fi
|
||||||
|
|
||||||
|
start-cli --registry=https://alpha-registry-x.start9.com registry os version add $VERSION "v$VERSION" '' ">=0.3.5 <=$VERSION"
|
||||||
|
|
||||||
|
if [ "$SKIP_UL" = "2" ]; then
|
||||||
|
exit 2
|
||||||
|
elif [ "$SKIP_UL" != "1" ]; then
|
||||||
|
for file in *.deb start-cli_*; do
|
||||||
|
gh release upload -R Start9Labs/start-os v$VERSION $file
|
||||||
|
done
|
||||||
|
for file in *.iso *.squashfs; do
|
||||||
|
s3cmd put -P $file s3://startos-images/v$VERSION/$file
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$SKIP_INDEX" != "1" ]; then
|
||||||
|
for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do
|
||||||
|
for file in *_$arch.squashfs *_$arch.iso; do
|
||||||
|
start-cli --registry=https://alpha-registry-x.start9.com registry os asset add --platform=$arch --version=$VERSION $file https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$file
|
||||||
|
done
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
for file in *.iso *.squashfs *.deb start-cli_*; do
|
||||||
|
gpg -u 7CFFDA41CA66056A --detach-sign --armor -o "${file}.asc" "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
gpg --export -a 7CFFDA41CA66056A > dr-bonez.key.asc
|
||||||
|
tar -czvf signatures.tar.gz *.asc
|
||||||
|
|
||||||
|
gh release upload -R Start9Labs/start-os v$VERSION signatures.tar.gz
|
||||||
|
|
||||||
|
cat << EOF
|
||||||
|
# ISO Downloads
|
||||||
|
|
||||||
|
- [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")
|
||||||
|
- [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")
|
||||||
|
- [RISCV64 (RVA23)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_riscv64.iso))
|
||||||
|
|
||||||
|
EOF
|
||||||
|
cat << 'EOF'
|
||||||
|
# StartOS Checksums
|
||||||
|
|
||||||
|
## SHA-256
|
||||||
|
```
|
||||||
|
EOF
|
||||||
|
sha256sum *.iso *.squashfs
|
||||||
|
cat << 'EOF'
|
||||||
|
```
|
||||||
|
|
||||||
|
## BLAKE-3
|
||||||
|
```
|
||||||
|
EOF
|
||||||
|
b3sum *.iso *.squashfs
|
||||||
|
cat << 'EOF'
|
||||||
|
```
|
||||||
|
|
||||||
|
# Start-Tunnel Checksums
|
||||||
|
|
||||||
|
## SHA-256
|
||||||
|
```
|
||||||
|
EOF
|
||||||
|
sha256sum start-tunnel*.deb
|
||||||
|
cat << 'EOF'
|
||||||
|
```
|
||||||
|
|
||||||
|
## BLAKE-3
|
||||||
|
```
|
||||||
|
EOF
|
||||||
|
b3sum start-tunnel*.deb
|
||||||
|
cat << 'EOF'
|
||||||
|
```
|
||||||
|
|
||||||
|
# start-cli Checksums
|
||||||
|
|
||||||
|
## SHA-256
|
||||||
|
```
|
||||||
|
EOF
|
||||||
|
sha256sum start-cli_*
|
||||||
|
cat << 'EOF'
|
||||||
|
```
|
||||||
|
|
||||||
|
## BLAKE-3
|
||||||
|
```
|
||||||
|
EOF
|
||||||
|
b3sum start-cli_*
|
||||||
|
cat << 'EOF'
|
||||||
|
```
|
||||||
|
EOF
|
||||||
32
container-runtime/CLAUDE.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Container Runtime — Node.js Service Manager
|
||||||
|
|
||||||
|
Node.js runtime that manages service containers via JSON-RPC. See `RPCSpec.md` in this directory for the full RPC protocol.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
LXC Container (uniform base for all services)
|
||||||
|
└── systemd
|
||||||
|
└── container-runtime.service
|
||||||
|
└── Loads /usr/lib/startos/package/index.js (from s9pk javascript.squashfs)
|
||||||
|
└── Package JS launches subcontainers (from images in s9pk)
|
||||||
|
```
|
||||||
|
|
||||||
|
The container runtime communicates with the host via JSON-RPC over Unix socket. Package JavaScript must export functions conforming to the `ABI` type defined in `sdk/base/lib/types.ts`.
|
||||||
|
|
||||||
|
## `/media/startos/` Directory (mounted by host into container)
|
||||||
|
|
||||||
|
| Path | Description |
|
||||||
|
| -------------------- | ----------------------------------------------------- |
|
||||||
|
| `volumes/<name>/` | Package data volumes (id-mapped, persistent) |
|
||||||
|
| `assets/` | Read-only assets from s9pk `assets.squashfs` |
|
||||||
|
| `images/<name>/` | Container images (squashfs, used for subcontainers) |
|
||||||
|
| `images/<name>.env` | Environment variables for image |
|
||||||
|
| `images/<name>.json` | Image metadata |
|
||||||
|
| `backup/` | Backup mount point (mounted during backup operations) |
|
||||||
|
| `rpc/service.sock` | RPC socket (container runtime listens here) |
|
||||||
|
| `rpc/host.sock` | Host RPC socket (for effects callbacks to host) |
|
||||||
|
|
||||||
|
## S9PK Structure
|
||||||
|
|
||||||
|
See `../core/s9pk-structure.md` for the S9PK package format.
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
# Container RPC SERVER Specification
|
# Container RPC Server Specification
|
||||||
|
|
||||||
|
The container runtime exposes a JSON-RPC server over a Unix socket at `/media/startos/rpc/service.sock`.
|
||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
### init
|
### init
|
||||||
|
|
||||||
initialize runtime (mount `/proc`, `/sys`, `/dev`, and `/run` to each image in `/media/images`)
|
Initialize the runtime and system.
|
||||||
|
|
||||||
called after os has mounted js and images to the container
|
#### params
|
||||||
|
|
||||||
#### args
|
```ts
|
||||||
|
{
|
||||||
`[]`
|
id: string,
|
||||||
|
kind: "install" | "update" | "restore" | null,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### response
|
#### response
|
||||||
|
|
||||||
@@ -18,11 +23,16 @@ called after os has mounted js and images to the container
|
|||||||
|
|
||||||
### exit
|
### exit
|
||||||
|
|
||||||
shutdown runtime
|
Shutdown runtime and optionally run exit hooks for a target version.
|
||||||
|
|
||||||
#### args
|
#### params
|
||||||
|
|
||||||
`[]`
|
```ts
|
||||||
|
{
|
||||||
|
id: string,
|
||||||
|
target: string | null, // ExtendedVersion or VersionRange
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### response
|
#### response
|
||||||
|
|
||||||
@@ -30,11 +40,11 @@ shutdown runtime
|
|||||||
|
|
||||||
### start
|
### start
|
||||||
|
|
||||||
run main method if not already running
|
Run main method if not already running.
|
||||||
|
|
||||||
#### args
|
#### params
|
||||||
|
|
||||||
`[]`
|
None
|
||||||
|
|
||||||
#### response
|
#### response
|
||||||
|
|
||||||
@@ -42,11 +52,11 @@ run main method if not already running
|
|||||||
|
|
||||||
### stop
|
### stop
|
||||||
|
|
||||||
stop main method by sending SIGTERM to child processes, and SIGKILL after timeout
|
Stop main method by sending SIGTERM to child processes, and SIGKILL after timeout.
|
||||||
|
|
||||||
#### args
|
#### params
|
||||||
|
|
||||||
`{ timeout: millis }`
|
None
|
||||||
|
|
||||||
#### response
|
#### response
|
||||||
|
|
||||||
@@ -54,15 +64,16 @@ stop main method by sending SIGTERM to child processes, and SIGKILL after timeou
|
|||||||
|
|
||||||
### execute
|
### execute
|
||||||
|
|
||||||
run a specific package procedure
|
Run a specific package procedure.
|
||||||
|
|
||||||
#### args
|
#### params
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
{
|
{
|
||||||
procedure: JsonPath,
|
id: string, // event ID
|
||||||
input: any,
|
procedure: string, // JSON path (e.g., "/backup/create", "/actions/{name}/run")
|
||||||
timeout: millis,
|
input: any,
|
||||||
|
timeout: number | null,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -72,18 +83,64 @@ run a specific package procedure
|
|||||||
|
|
||||||
### sandbox
|
### sandbox
|
||||||
|
|
||||||
run a specific package procedure in sandbox mode
|
Run a specific package procedure in sandbox mode. Same interface as `execute`.
|
||||||
|
|
||||||
#### args
|
UNIMPLEMENTED: this feature is planned but does not exist
|
||||||
|
|
||||||
|
#### params
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
{
|
{
|
||||||
procedure: JsonPath,
|
id: string,
|
||||||
input: any,
|
procedure: string,
|
||||||
timeout: millis,
|
input: any,
|
||||||
|
timeout: number | null,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### response
|
#### response
|
||||||
|
|
||||||
`any`
|
`any`
|
||||||
|
|
||||||
|
### callback
|
||||||
|
|
||||||
|
Handle a callback from an effect.
|
||||||
|
|
||||||
|
#### params
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
id: number,
|
||||||
|
args: any[],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### response
|
||||||
|
|
||||||
|
`null` (no response sent)
|
||||||
|
|
||||||
|
### eval
|
||||||
|
|
||||||
|
Evaluate a script in the runtime context. Used for debugging.
|
||||||
|
|
||||||
|
#### params
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
script: string,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### response
|
||||||
|
|
||||||
|
`any`
|
||||||
|
|
||||||
|
## Procedures
|
||||||
|
|
||||||
|
The `execute` and `sandbox` methods route to procedures based on the `procedure` path:
|
||||||
|
|
||||||
|
| Procedure | Description |
|
||||||
|
| -------------------------- | ---------------------------- |
|
||||||
|
| `/backup/create` | Create a backup |
|
||||||
|
| `/actions/{name}/getInput` | Get input spec for an action |
|
||||||
|
| `/actions/{name}/run` | Run an action with input |
|
||||||
|
|||||||
6
container-runtime/container-runtime-failure.service
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=StartOS Container Runtime Failure Handler
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/start-container rebuild
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=StartOS Container Runtime
|
Description=StartOS Container Runtime
|
||||||
|
OnFailure=container-runtime-failure.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=/usr/bin/node --experimental-detect-module --unhandled-rejections=warn /usr/lib/startos/init/index.js
|
Environment=RUST_LOG=startos=debug
|
||||||
Restart=always
|
ExecStart=/usr/bin/node --experimental-detect-module --trace-warnings --unhandled-rejections=warn /usr/lib/startos/init/index.js
|
||||||
RestartSec=3
|
Restart=no
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
@@ -2,17 +2,10 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
mkdir -p /run/systemd/resolve
|
|
||||||
echo "nameserver 8.8.8.8" > /run/systemd/resolve/stub-resolv.conf
|
|
||||||
|
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y curl rsync qemu-user-static
|
apt-get install -y curl rsync qemu-user-static nodejs
|
||||||
|
|
||||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
|
||||||
source ~/.bashrc
|
|
||||||
nvm install 20
|
|
||||||
ln -s $(which node) /usr/bin/node
|
|
||||||
|
|
||||||
|
sed -i '/\(^\|#\)DNSStubListener=/c\DNSStubListener=no' /etc/systemd/resolved.conf
|
||||||
sed -i '/\(^\|#\)Storage=/c\Storage=persistent' /etc/systemd/journald.conf
|
sed -i '/\(^\|#\)Storage=/c\Storage=persistent' /etc/systemd/journald.conf
|
||||||
sed -i '/\(^\|#\)Compress=/c\Compress=yes' /etc/systemd/journald.conf
|
sed -i '/\(^\|#\)Compress=/c\Compress=yes' /etc/systemd/journald.conf
|
||||||
sed -i '/\(^\|#\)SystemMaxUse=/c\SystemMaxUse=1G' /etc/systemd/journald.conf
|
sed -i '/\(^\|#\)SystemMaxUse=/c\SystemMaxUse=1G' /etc/systemd/journald.conf
|
||||||
@@ -20,4 +13,4 @@ sed -i '/\(^\|#\)ForwardToSyslog=/c\ForwardToSyslog=no' /etc/systemd/journald.co
|
|||||||
|
|
||||||
systemctl enable container-runtime.service
|
systemctl enable container-runtime.service
|
||||||
|
|
||||||
rm -rf /run/systemd
|
echo "nameserver 10.0.3.1" > /etc/resolv.conf
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
DISTRO=debian
|
DISTRO=debian
|
||||||
VERSION=bookworm
|
VERSION=trixie
|
||||||
ARCH=${ARCH:-$(uname -m)}
|
ARCH=${ARCH:-$(uname -m)}
|
||||||
FLAVOR=default
|
FLAVOR=default
|
||||||
|
|
||||||
@@ -16,8 +14,9 @@ elif [ "$_ARCH" = "aarch64" ]; then
|
|||||||
_ARCH=arm64
|
_ARCH=arm64
|
||||||
fi
|
fi
|
||||||
|
|
||||||
URL="https://images.linuxcontainers.org/$(curl -fsSL https://images.linuxcontainers.org/meta/1.0/index-system | grep "^$DISTRO;$VERSION;$_ARCH;$FLAVOR;" | head -n1 | sed 's/^.*;//g')/rootfs.squashfs"
|
BASE_URL="https://images.linuxcontainers.org$(curl -fsSL https://images.linuxcontainers.org/meta/1.0/index-system | grep "^$DISTRO;$VERSION;$_ARCH;$FLAVOR;" | head -n1 | sed 's/^.*;//g')"
|
||||||
|
OUTPUT_FILE="debian.${ARCH}.squashfs"
|
||||||
|
|
||||||
echo "Downloading $URL to debian.${ARCH}.squashfs"
|
echo "Downloading ${BASE_URL}/rootfs.squashfs to $OUTPUT_FILE"
|
||||||
|
curl -fsSL "${BASE_URL}/rootfs.squashfs" > "$OUTPUT_FILE"
|
||||||
curl -fsSL "$URL" > debian.${ARCH}.squashfs
|
curl -fsSL "$BASE_URL/SHA256SUMS" | grep 'rootfs\.squashfs' | awk '{print $1" '"$OUTPUT_FILE"'"}' | shasum -a 256 -c
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
IMAGE=$1
|
|
||||||
|
|
||||||
if [ -z "$IMAGE" ]; then
|
|
||||||
>&2 echo "usage: $0 <image id>"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! [ -d "/media/images/$IMAGE" ]; then
|
|
||||||
>&2 echo "image does not exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
container=$(mktemp -d)
|
|
||||||
mkdir -p $container/rootfs $container/upper $container/work
|
|
||||||
mount -t overlay -olowerdir=/media/images/$IMAGE,upperdir=$container/upper,workdir=$container/work overlay $container/rootfs
|
|
||||||
|
|
||||||
rootfs=$container/rootfs
|
|
||||||
|
|
||||||
for special in dev sys proc run; do
|
|
||||||
mkdir -p $rootfs/$special
|
|
||||||
mount --bind /$special $rootfs/$special
|
|
||||||
done
|
|
||||||
|
|
||||||
echo $rootfs
|
|
||||||
6090
container-runtime/package-lock.json
generated
@@ -26,8 +26,8 @@
|
|||||||
"isomorphic-fetch": "^3.0.0",
|
"isomorphic-fetch": "^3.0.0",
|
||||||
"jsonpath": "^1.1.1",
|
"jsonpath": "^1.1.1",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
|
"mime": "^4.0.7",
|
||||||
"node-fetch": "^3.1.0",
|
"node-fetch": "^3.1.0",
|
||||||
"ts-matches": "^5.5.1",
|
|
||||||
"tslib": "^2.5.3",
|
"tslib": "^2.5.3",
|
||||||
"typescript": "^5.1.3",
|
"typescript": "^5.1.3",
|
||||||
"yaml": "^2.3.1"
|
"yaml": "^2.3.1"
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
rootfs=$1
|
|
||||||
if [ -z "$rootfs" ]; then
|
|
||||||
>&2 echo "usage: $0 <container rootfs path>"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
umount --recursive $rootfs
|
|
||||||
rm -rf $rootfs/..
|
|
||||||
@@ -1,46 +1,53 @@
|
|||||||
import { types as T, utils } from "@start9labs/start-sdk"
|
import {
|
||||||
|
ExtendedVersion,
|
||||||
|
types as T,
|
||||||
|
utils,
|
||||||
|
VersionRange,
|
||||||
|
z,
|
||||||
|
} from "@start9labs/start-sdk"
|
||||||
import * as net from "net"
|
import * as net from "net"
|
||||||
import { object, string, number, literals, some, unknown } from "ts-matches"
|
|
||||||
import { Effects } from "../Models/Effects"
|
import { Effects } from "../Models/Effects"
|
||||||
|
|
||||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||||
import { asError } from "@start9labs/start-sdk/base/lib/util"
|
import { asError } from "@start9labs/start-sdk/base/lib/util"
|
||||||
const matchRpcError = object({
|
const matchRpcError = z.object({
|
||||||
error: object(
|
error: z.object({
|
||||||
{
|
code: z.number(),
|
||||||
code: number,
|
message: z.string(),
|
||||||
message: string,
|
data: z
|
||||||
data: some(
|
.union([
|
||||||
string,
|
z.string(),
|
||||||
object(
|
z.object({
|
||||||
{
|
details: z.string(),
|
||||||
details: string,
|
debug: z.string().nullable().optional(),
|
||||||
debug: string,
|
}),
|
||||||
},
|
])
|
||||||
["debug"],
|
.nullable()
|
||||||
),
|
.optional(),
|
||||||
),
|
}),
|
||||||
},
|
|
||||||
["data"],
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
const testRpcError = matchRpcError.test
|
function testRpcError(v: unknown): v is RpcError {
|
||||||
const testRpcResult = object({
|
return matchRpcError.safeParse(v).success
|
||||||
result: unknown,
|
}
|
||||||
}).test
|
const matchRpcResult = z.object({
|
||||||
type RpcError = typeof matchRpcError._TYPE
|
result: z.unknown(),
|
||||||
|
})
|
||||||
|
function testRpcResult(v: unknown): v is z.infer<typeof matchRpcResult> {
|
||||||
|
return matchRpcResult.safeParse(v).success
|
||||||
|
}
|
||||||
|
type RpcError = z.infer<typeof matchRpcError>
|
||||||
|
|
||||||
const SOCKET_PATH = "/media/startos/rpc/host.sock"
|
const SOCKET_PATH = "/media/startos/rpc/host.sock"
|
||||||
let hostSystemId = 0
|
let hostSystemId = 0
|
||||||
|
|
||||||
export type EffectContext = {
|
export type EffectContext = {
|
||||||
procedureId: string | null
|
eventId: string | null
|
||||||
callbacks?: CallbackHolder
|
callbacks?: CallbackHolder
|
||||||
constRetry?: () => void
|
constRetry?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const rpcRoundFor =
|
const rpcRoundFor =
|
||||||
(procedureId: string | null) =>
|
(eventId: string | null) =>
|
||||||
<K extends T.EffectMethod | "clearCallbacks">(
|
<K extends T.EffectMethod | "clearCallbacks">(
|
||||||
method: K,
|
method: K,
|
||||||
params: Record<string, unknown>,
|
params: Record<string, unknown>,
|
||||||
@@ -51,7 +58,7 @@ const rpcRoundFor =
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
id,
|
id,
|
||||||
method,
|
method,
|
||||||
params: { ...params, procedureId: procedureId || undefined },
|
params: { ...params, eventId: eventId ?? undefined },
|
||||||
}) + "\n",
|
}) + "\n",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -70,7 +77,7 @@ const rpcRoundFor =
|
|||||||
"Error in host RPC:",
|
"Error in host RPC:",
|
||||||
utils.asError({ method, params, error: res.error }),
|
utils.asError({ method, params, error: res.error }),
|
||||||
)
|
)
|
||||||
if (string.test(res.error.data)) {
|
if (typeof res.error.data === "string") {
|
||||||
message += ": " + res.error.data
|
message += ": " + res.error.data
|
||||||
console.error(`Details: ${res.error.data}`)
|
console.error(`Details: ${res.error.data}`)
|
||||||
} else {
|
} else {
|
||||||
@@ -102,9 +109,21 @@ const rpcRoundFor =
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function makeEffects(context: EffectContext): Effects {
|
export function makeEffects(context: EffectContext): Effects {
|
||||||
const rpcRound = rpcRoundFor(context.procedureId)
|
const rpcRound = rpcRoundFor(context.eventId)
|
||||||
const self: Effects = {
|
const self: Effects = {
|
||||||
|
eventId: context.eventId,
|
||||||
|
child: (name) =>
|
||||||
|
makeEffects({ ...context, callbacks: context.callbacks?.child(name) }),
|
||||||
constRetry: context.constRetry,
|
constRetry: context.constRetry,
|
||||||
|
isInContext: !!context.callbacks,
|
||||||
|
onLeaveContext:
|
||||||
|
context.callbacks?.onLeaveContext?.bind(context.callbacks) ||
|
||||||
|
(() => {
|
||||||
|
console.warn(
|
||||||
|
"no context for this effects object",
|
||||||
|
new Error().stack?.replace(/^Error/, ""),
|
||||||
|
)
|
||||||
|
}),
|
||||||
clearCallbacks(...[options]: Parameters<T.Effects["clearCallbacks"]>) {
|
clearCallbacks(...[options]: Parameters<T.Effects["clearCallbacks"]>) {
|
||||||
return rpcRound("clear-callbacks", {
|
return rpcRound("clear-callbacks", {
|
||||||
...options,
|
...options,
|
||||||
@@ -126,22 +145,20 @@ export function makeEffects(context: EffectContext): Effects {
|
|||||||
...options,
|
...options,
|
||||||
}) as ReturnType<T.Effects["action"]["getInput"]>
|
}) as ReturnType<T.Effects["action"]["getInput"]>
|
||||||
},
|
},
|
||||||
request(...[options]: Parameters<T.Effects["action"]["request"]>) {
|
createTask(...[options]: Parameters<T.Effects["action"]["createTask"]>) {
|
||||||
return rpcRound("action.request", {
|
return rpcRound("action.create-task", {
|
||||||
...options,
|
...options,
|
||||||
}) as ReturnType<T.Effects["action"]["request"]>
|
}) as ReturnType<T.Effects["action"]["createTask"]>
|
||||||
},
|
},
|
||||||
run(...[options]: Parameters<T.Effects["action"]["run"]>) {
|
run(...[options]: Parameters<T.Effects["action"]["run"]>) {
|
||||||
return rpcRound("action.run", {
|
return rpcRound("action.run", {
|
||||||
...options,
|
...options,
|
||||||
}) as ReturnType<T.Effects["action"]["run"]>
|
}) as ReturnType<T.Effects["action"]["run"]>
|
||||||
},
|
},
|
||||||
clearRequests(
|
clearTasks(...[options]: Parameters<T.Effects["action"]["clearTasks"]>) {
|
||||||
...[options]: Parameters<T.Effects["action"]["clearRequests"]>
|
return rpcRound("action.clear-tasks", {
|
||||||
) {
|
|
||||||
return rpcRound("action.clear-requests", {
|
|
||||||
...options,
|
...options,
|
||||||
}) as ReturnType<T.Effects["action"]["clearRequests"]>
|
}) as ReturnType<T.Effects["action"]["clearTasks"]>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bind(...[options]: Parameters<T.Effects["bind"]>) {
|
bind(...[options]: Parameters<T.Effects["bind"]>) {
|
||||||
@@ -167,6 +184,13 @@ export function makeEffects(context: EffectContext): Effects {
|
|||||||
T.Effects["getInstalledPackages"]
|
T.Effects["getInstalledPackages"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
|
getServiceManifest(
|
||||||
|
...[options]: Parameters<T.Effects["getServiceManifest"]>
|
||||||
|
) {
|
||||||
|
return rpcRound("get-service-manifest", options) as ReturnType<
|
||||||
|
T.Effects["getServiceManifest"]
|
||||||
|
>
|
||||||
|
},
|
||||||
subcontainer: {
|
subcontainer: {
|
||||||
createFs(options: { imageId: string; name: string }) {
|
createFs(options: { imageId: string; name: string }) {
|
||||||
return rpcRound("subcontainer.create-fs", options) as ReturnType<
|
return rpcRound("subcontainer.create-fs", options) as ReturnType<
|
||||||
@@ -186,13 +210,6 @@ export function makeEffects(context: EffectContext): Effects {
|
|||||||
T.Effects["exportServiceInterface"]
|
T.Effects["exportServiceInterface"]
|
||||||
>
|
>
|
||||||
}) as Effects["exportServiceInterface"],
|
}) as Effects["exportServiceInterface"],
|
||||||
exposeForDependents(
|
|
||||||
...[options]: Parameters<T.Effects["exposeForDependents"]>
|
|
||||||
) {
|
|
||||||
return rpcRound("expose-for-dependents", options) as ReturnType<
|
|
||||||
T.Effects["exposeForDependents"]
|
|
||||||
>
|
|
||||||
},
|
|
||||||
getContainerIp(...[options]: Parameters<T.Effects["getContainerIp"]>) {
|
getContainerIp(...[options]: Parameters<T.Effects["getContainerIp"]>) {
|
||||||
return rpcRound("get-container-ip", options) as ReturnType<
|
return rpcRound("get-container-ip", options) as ReturnType<
|
||||||
T.Effects["getContainerIp"]
|
T.Effects["getContainerIp"]
|
||||||
@@ -242,6 +259,14 @@ export function makeEffects(context: EffectContext): Effects {
|
|||||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||||
}) as ReturnType<T.Effects["getSystemSmtp"]>
|
}) as ReturnType<T.Effects["getSystemSmtp"]>
|
||||||
},
|
},
|
||||||
|
getOutboundGateway(
|
||||||
|
...[options]: Parameters<T.Effects["getOutboundGateway"]>
|
||||||
|
) {
|
||||||
|
return rpcRound("get-outbound-gateway", {
|
||||||
|
...options,
|
||||||
|
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||||
|
}) as ReturnType<T.Effects["getOutboundGateway"]>
|
||||||
|
},
|
||||||
listServiceInterfaces(
|
listServiceInterfaces(
|
||||||
...[options]: Parameters<T.Effects["listServiceInterfaces"]>
|
...[options]: Parameters<T.Effects["listServiceInterfaces"]>
|
||||||
) {
|
) {
|
||||||
@@ -254,6 +279,7 @@ export function makeEffects(context: EffectContext): Effects {
|
|||||||
return rpcRound("mount", options) as ReturnType<T.Effects["mount"]>
|
return rpcRound("mount", options) as ReturnType<T.Effects["mount"]>
|
||||||
},
|
},
|
||||||
restart(...[]: Parameters<T.Effects["restart"]>) {
|
restart(...[]: Parameters<T.Effects["restart"]>) {
|
||||||
|
console.log("Restarting service...")
|
||||||
return rpcRound("restart", {}) as ReturnType<T.Effects["restart"]>
|
return rpcRound("restart", {}) as ReturnType<T.Effects["restart"]>
|
||||||
},
|
},
|
||||||
setDependencies(
|
setDependencies(
|
||||||
@@ -284,6 +310,7 @@ export function makeEffects(context: EffectContext): Effects {
|
|||||||
getStatus(...[o]: Parameters<T.Effects["getStatus"]>) {
|
getStatus(...[o]: Parameters<T.Effects["getStatus"]>) {
|
||||||
return rpcRound("get-status", o) as ReturnType<T.Effects["getStatus"]>
|
return rpcRound("get-status", o) as ReturnType<T.Effects["getStatus"]>
|
||||||
},
|
},
|
||||||
|
/// DEPRECATED
|
||||||
setMainStatus(o: { status: "running" | "stopped" }): Promise<null> {
|
setMainStatus(o: { status: "running" | "stopped" }): Promise<null> {
|
||||||
return rpcRound("set-main-status", o) as ReturnType<
|
return rpcRound("set-main-status", o) as ReturnType<
|
||||||
T.Effects["setHealth"]
|
T.Effects["setHealth"]
|
||||||
@@ -293,15 +320,6 @@ export function makeEffects(context: EffectContext): Effects {
|
|||||||
shutdown(...[]: Parameters<T.Effects["shutdown"]>) {
|
shutdown(...[]: Parameters<T.Effects["shutdown"]>) {
|
||||||
return rpcRound("shutdown", {}) as ReturnType<T.Effects["shutdown"]>
|
return rpcRound("shutdown", {}) as ReturnType<T.Effects["shutdown"]>
|
||||||
},
|
},
|
||||||
store: {
|
|
||||||
get: async (options: any) =>
|
|
||||||
rpcRound("store.get", {
|
|
||||||
...options,
|
|
||||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
|
||||||
}) as any,
|
|
||||||
set: async (options: any) =>
|
|
||||||
rpcRound("store.set", options) as ReturnType<T.Effects["store"]["set"]>,
|
|
||||||
} as T.Effects["store"],
|
|
||||||
getDataVersion() {
|
getDataVersion() {
|
||||||
return rpcRound("get-data-version", {}) as ReturnType<
|
return rpcRound("get-data-version", {}) as ReturnType<
|
||||||
T.Effects["getDataVersion"]
|
T.Effects["getDataVersion"]
|
||||||
@@ -312,6 +330,42 @@ export function makeEffects(context: EffectContext): Effects {
|
|||||||
T.Effects["setDataVersion"]
|
T.Effects["setDataVersion"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
|
plugin: {
|
||||||
|
url: {
|
||||||
|
register(
|
||||||
|
...[options]: Parameters<T.Effects["plugin"]["url"]["register"]>
|
||||||
|
) {
|
||||||
|
return rpcRound("plugin.url.register", options) as ReturnType<
|
||||||
|
T.Effects["plugin"]["url"]["register"]
|
||||||
|
>
|
||||||
|
},
|
||||||
|
exportUrl(
|
||||||
|
...[options]: Parameters<T.Effects["plugin"]["url"]["exportUrl"]>
|
||||||
|
) {
|
||||||
|
return rpcRound("plugin.url.export-url", options) as ReturnType<
|
||||||
|
T.Effects["plugin"]["url"]["exportUrl"]
|
||||||
|
>
|
||||||
|
},
|
||||||
|
clearUrls(
|
||||||
|
...[options]: Parameters<T.Effects["plugin"]["url"]["clearUrls"]>
|
||||||
|
) {
|
||||||
|
return rpcRound("plugin.url.clear-urls", options) as ReturnType<
|
||||||
|
T.Effects["plugin"]["url"]["clearUrls"]
|
||||||
|
>
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
if (context.callbacks?.onLeaveContext)
|
||||||
|
self.onLeaveContext(() => {
|
||||||
|
self.constRetry = undefined
|
||||||
|
self.isInContext = false
|
||||||
|
self.onLeaveContext = () => {
|
||||||
|
console.warn(
|
||||||
|
"this effects object is already out of context",
|
||||||
|
new Error().stack?.replace(/^Error/, ""),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import * as net from "net"
|
import * as net from "net"
|
||||||
import {
|
|
||||||
object,
|
|
||||||
some,
|
|
||||||
string,
|
|
||||||
literal,
|
|
||||||
array,
|
|
||||||
number,
|
|
||||||
matches,
|
|
||||||
any,
|
|
||||||
shape,
|
|
||||||
anyOf,
|
|
||||||
} from "ts-matches"
|
|
||||||
|
|
||||||
import { types as T, utils } from "@start9labs/start-sdk"
|
import {
|
||||||
|
ExtendedVersion,
|
||||||
|
types as T,
|
||||||
|
utils,
|
||||||
|
VersionRange,
|
||||||
|
z,
|
||||||
|
} from "@start9labs/start-sdk"
|
||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
|
|
||||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||||
@@ -23,114 +17,94 @@ import { jsonPath, unNestPath } from "../Models/JsonPath"
|
|||||||
import { System } from "../Interfaces/System"
|
import { System } from "../Interfaces/System"
|
||||||
import { makeEffects } from "./EffectCreator"
|
import { makeEffects } from "./EffectCreator"
|
||||||
type MaybePromise<T> = T | Promise<T>
|
type MaybePromise<T> = T | Promise<T>
|
||||||
export const matchRpcResult = anyOf(
|
export const matchRpcResult = z.union([
|
||||||
object({ result: any }),
|
z.object({ result: z.any() }),
|
||||||
object({
|
z.object({
|
||||||
error: object(
|
error: z.object({
|
||||||
{
|
code: z.number(),
|
||||||
code: number,
|
message: z.string(),
|
||||||
message: string,
|
data: z
|
||||||
data: object(
|
.object({
|
||||||
{
|
details: z.string().optional(),
|
||||||
details: string,
|
debug: z.any().optional(),
|
||||||
debug: any,
|
})
|
||||||
},
|
.nullable()
|
||||||
["details", "debug"],
|
.optional(),
|
||||||
),
|
}),
|
||||||
},
|
|
||||||
["data"],
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
)
|
])
|
||||||
|
|
||||||
export type RpcResult = typeof matchRpcResult._TYPE
|
export type RpcResult = z.infer<typeof matchRpcResult>
|
||||||
type SocketResponse = ({ jsonrpc: "2.0"; id: IdType } & RpcResult) | null
|
type SocketResponse = ({ jsonrpc: "2.0"; id: IdType } & RpcResult) | null
|
||||||
|
|
||||||
const SOCKET_PARENT = "/media/startos/rpc"
|
const SOCKET_PARENT = "/media/startos/rpc"
|
||||||
const SOCKET_PATH = "/media/startos/rpc/service.sock"
|
const SOCKET_PATH = "/media/startos/rpc/service.sock"
|
||||||
const jsonrpc = "2.0" as const
|
const jsonrpc = "2.0" as const
|
||||||
|
|
||||||
const isResult = object({ result: any }).test
|
const isResultSchema = z.object({ result: z.any() })
|
||||||
|
const isResult = (v: unknown): v is z.infer<typeof isResultSchema> =>
|
||||||
|
isResultSchema.safeParse(v).success
|
||||||
|
|
||||||
const idType = some(string, number, literal(null))
|
const idType = z.union([z.string(), z.number(), z.literal(null)])
|
||||||
type IdType = null | string | number | undefined
|
type IdType = null | string | number | undefined
|
||||||
const runType = object(
|
const runType = z.object({
|
||||||
{
|
id: idType.optional(),
|
||||||
id: idType,
|
method: z.literal("execute"),
|
||||||
method: literal("execute"),
|
params: z.object({
|
||||||
params: object(
|
id: z.string(),
|
||||||
{
|
procedure: z.string(),
|
||||||
id: string,
|
input: z.any(),
|
||||||
procedure: string,
|
timeout: z.number().nullable().optional(),
|
||||||
input: any,
|
}),
|
||||||
timeout: number,
|
})
|
||||||
},
|
const sandboxRunType = z.object({
|
||||||
["timeout"],
|
id: idType.optional(),
|
||||||
),
|
method: z.literal("sandbox"),
|
||||||
},
|
params: z.object({
|
||||||
["id"],
|
id: z.string(),
|
||||||
)
|
procedure: z.string(),
|
||||||
const sandboxRunType = object(
|
input: z.any(),
|
||||||
{
|
timeout: z.number().nullable().optional(),
|
||||||
id: idType,
|
}),
|
||||||
method: literal("sandbox"),
|
})
|
||||||
params: object(
|
const callbackType = z.object({
|
||||||
{
|
method: z.literal("callback"),
|
||||||
id: string,
|
params: z.object({
|
||||||
procedure: string,
|
id: z.number(),
|
||||||
input: any,
|
args: z.array(z.unknown()),
|
||||||
timeout: number,
|
}),
|
||||||
},
|
})
|
||||||
["timeout"],
|
const initType = z.object({
|
||||||
),
|
id: idType.optional(),
|
||||||
},
|
method: z.literal("init"),
|
||||||
["id"],
|
params: z.object({
|
||||||
)
|
id: z.string(),
|
||||||
const callbackType = object({
|
kind: z.enum(["install", "update", "restore"]).nullable(),
|
||||||
method: literal("callback"),
|
}),
|
||||||
params: object({
|
})
|
||||||
id: number,
|
const startType = z.object({
|
||||||
args: array,
|
id: idType.optional(),
|
||||||
|
method: z.literal("start"),
|
||||||
|
})
|
||||||
|
const stopType = z.object({
|
||||||
|
id: idType.optional(),
|
||||||
|
method: z.literal("stop"),
|
||||||
|
})
|
||||||
|
const exitType = z.object({
|
||||||
|
id: idType.optional(),
|
||||||
|
method: z.literal("exit"),
|
||||||
|
params: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
target: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const evalType = z.object({
|
||||||
|
id: idType.optional(),
|
||||||
|
method: z.literal("eval"),
|
||||||
|
params: z.object({
|
||||||
|
script: z.string(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
const initType = object(
|
|
||||||
{
|
|
||||||
id: idType,
|
|
||||||
method: literal("init"),
|
|
||||||
},
|
|
||||||
["id"],
|
|
||||||
)
|
|
||||||
const startType = object(
|
|
||||||
{
|
|
||||||
id: idType,
|
|
||||||
method: literal("start"),
|
|
||||||
},
|
|
||||||
["id"],
|
|
||||||
)
|
|
||||||
const stopType = object(
|
|
||||||
{
|
|
||||||
id: idType,
|
|
||||||
method: literal("stop"),
|
|
||||||
},
|
|
||||||
["id"],
|
|
||||||
)
|
|
||||||
const exitType = object(
|
|
||||||
{
|
|
||||||
id: idType,
|
|
||||||
method: literal("exit"),
|
|
||||||
},
|
|
||||||
["id"],
|
|
||||||
)
|
|
||||||
const evalType = object(
|
|
||||||
{
|
|
||||||
id: idType,
|
|
||||||
method: literal("eval"),
|
|
||||||
params: object({
|
|
||||||
script: string,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
["id"],
|
|
||||||
)
|
|
||||||
|
|
||||||
const jsonParse = (x: string) => JSON.parse(x)
|
const jsonParse = (x: string) => JSON.parse(x)
|
||||||
|
|
||||||
@@ -161,8 +135,11 @@ const handleRpc = (id: IdType, result: Promise<RpcResult>) =>
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const hasId = object({ id: idType }).test
|
const hasIdSchema = z.object({ id: idType })
|
||||||
|
const hasId = (v: unknown): v is z.infer<typeof hasIdSchema> =>
|
||||||
|
hasIdSchema.safeParse(v).success
|
||||||
export class RpcListener {
|
export class RpcListener {
|
||||||
|
shouldExit = false
|
||||||
unixSocketServer = net.createServer(async (server) => {})
|
unixSocketServer = net.createServer(async (server) => {})
|
||||||
private _system: System | undefined
|
private _system: System | undefined
|
||||||
private callbacks: CallbackHolder | undefined
|
private callbacks: CallbackHolder | undefined
|
||||||
@@ -171,8 +148,12 @@ export class RpcListener {
|
|||||||
if (!fs.existsSync(SOCKET_PARENT)) {
|
if (!fs.existsSync(SOCKET_PARENT)) {
|
||||||
fs.mkdirSync(SOCKET_PARENT, { recursive: true })
|
fs.mkdirSync(SOCKET_PARENT, { recursive: true })
|
||||||
}
|
}
|
||||||
|
if (fs.existsSync(SOCKET_PATH)) fs.rmSync(SOCKET_PATH, { force: true })
|
||||||
|
|
||||||
this.unixSocketServer.listen(SOCKET_PATH)
|
this.unixSocketServer.listen(SOCKET_PATH)
|
||||||
|
|
||||||
|
console.log("Listening on %s", SOCKET_PATH)
|
||||||
|
|
||||||
this.unixSocketServer.on("connection", (s) => {
|
this.unixSocketServer.on("connection", (s) => {
|
||||||
let id: IdType = null
|
let id: IdType = null
|
||||||
const captureId = <X>(x: X) => {
|
const captureId = <X>(x: X) => {
|
||||||
@@ -223,6 +204,11 @@ export class RpcListener {
|
|||||||
.catch(mapError)
|
.catch(mapError)
|
||||||
.then(logData("response"))
|
.then(logData("response"))
|
||||||
.then(writeDataToSocket)
|
.then(writeDataToSocket)
|
||||||
|
.then((_) => {
|
||||||
|
if (this.shouldExit) {
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(`Major error in socket handling: ${e}`)
|
console.error(`Major error in socket handling: ${e}`)
|
||||||
console.debug(`Data in: ${a.toString()}`)
|
console.debug(`Data in: ${a.toString()}`)
|
||||||
@@ -238,21 +224,6 @@ export class RpcListener {
|
|||||||
return this._system
|
return this._system
|
||||||
}
|
}
|
||||||
|
|
||||||
private callbackHolders: Map<string, CallbackHolder> = new Map()
|
|
||||||
private removeCallbackHolderFor(procedure: string) {
|
|
||||||
const prev = this.callbackHolders.get(procedure)
|
|
||||||
if (prev) {
|
|
||||||
this.callbackHolders.delete(procedure)
|
|
||||||
this.callbacks?.removeChild(prev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private callbackHolderFor(procedure: string): CallbackHolder {
|
|
||||||
this.removeCallbackHolderFor(procedure)
|
|
||||||
const callbackHolder = this.callbacks!.child()
|
|
||||||
this.callbackHolders.set(procedure, callbackHolder)
|
|
||||||
return callbackHolder
|
|
||||||
}
|
|
||||||
|
|
||||||
callCallback(callback: number, args: any[]): void {
|
callCallback(callback: number, args: any[]): void {
|
||||||
if (this.callbacks) {
|
if (this.callbacks) {
|
||||||
this.callbacks
|
this.callbacks
|
||||||
@@ -268,66 +239,100 @@ export class RpcListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private dealWithInput(input: unknown): MaybePromise<SocketResponse> {
|
private dealWithInput(input: unknown): MaybePromise<SocketResponse> {
|
||||||
return matches(input)
|
const parsed = z.object({ method: z.string() }).safeParse(input)
|
||||||
.when(runType, async ({ id, params }) => {
|
if (!parsed.success) {
|
||||||
|
console.warn(
|
||||||
|
`Couldn't parse the following input ${JSON.stringify(input)}`,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
jsonrpc,
|
||||||
|
id: (input as any)?.id,
|
||||||
|
error: {
|
||||||
|
code: -32602,
|
||||||
|
message: "invalid params",
|
||||||
|
data: {
|
||||||
|
details: JSON.stringify(input),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (parsed.data.method) {
|
||||||
|
case "execute": {
|
||||||
|
const { id, params } = runType.parse(input)
|
||||||
const system = this.system
|
const system = this.system
|
||||||
const procedure = jsonPath.unsafeCast(params.procedure)
|
const procedure = jsonPath.parse(params.procedure)
|
||||||
const { input, timeout, id: procedureId } = params
|
const { input: inp, timeout, id: eventId } = params
|
||||||
const result = this.getResult(
|
const result = this.getResult(procedure, system, eventId, timeout, inp)
|
||||||
procedure,
|
|
||||||
system,
|
|
||||||
procedureId,
|
|
||||||
timeout,
|
|
||||||
input,
|
|
||||||
)
|
|
||||||
|
|
||||||
return handleRpc(id, result)
|
return handleRpc(id, result)
|
||||||
})
|
}
|
||||||
.when(sandboxRunType, async ({ id, params }) => {
|
case "sandbox": {
|
||||||
|
const { id, params } = sandboxRunType.parse(input)
|
||||||
const system = this.system
|
const system = this.system
|
||||||
const procedure = jsonPath.unsafeCast(params.procedure)
|
const procedure = jsonPath.parse(params.procedure)
|
||||||
const { input, timeout, id: procedureId } = params
|
const { input: inp, timeout, id: eventId } = params
|
||||||
const result = this.getResult(
|
const result = this.getResult(procedure, system, eventId, timeout, inp)
|
||||||
procedure,
|
|
||||||
system,
|
|
||||||
procedureId,
|
|
||||||
timeout,
|
|
||||||
input,
|
|
||||||
)
|
|
||||||
|
|
||||||
return handleRpc(id, result)
|
return handleRpc(id, result)
|
||||||
})
|
}
|
||||||
.when(callbackType, async ({ params: { id, args } }) => {
|
case "callback": {
|
||||||
|
const {
|
||||||
|
params: { id, args },
|
||||||
|
} = callbackType.parse(input)
|
||||||
this.callCallback(id, args)
|
this.callCallback(id, args)
|
||||||
return null
|
return null
|
||||||
})
|
}
|
||||||
.when(startType, async ({ id }) => {
|
case "start": {
|
||||||
const callbacks = this.callbackHolderFor("main")
|
const { id } = startType.parse(input)
|
||||||
|
const callbacks =
|
||||||
|
this.callbacks?.getChild("main") || this.callbacks?.child("main")
|
||||||
const effects = makeEffects({
|
const effects = makeEffects({
|
||||||
procedureId: null,
|
eventId: null,
|
||||||
callbacks,
|
callbacks,
|
||||||
})
|
})
|
||||||
return handleRpc(
|
return handleRpc(
|
||||||
id,
|
id,
|
||||||
this.system.start(effects).then((result) => ({ result })),
|
this.system.start(effects).then((result) => ({ result })),
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
.when(stopType, async ({ id }) => {
|
case "stop": {
|
||||||
this.removeCallbackHolderFor("main")
|
const { id } = stopType.parse(input)
|
||||||
return handleRpc(
|
return handleRpc(
|
||||||
id,
|
id,
|
||||||
this.system.stop().then((result) => ({ result })),
|
this.system.stop().then((result) => {
|
||||||
|
this.callbacks?.removeChild("main")
|
||||||
|
|
||||||
|
return { result }
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
.when(exitType, async ({ id }) => {
|
case "exit": {
|
||||||
|
const { id, params } = exitType.parse(input)
|
||||||
return handleRpc(
|
return handleRpc(
|
||||||
id,
|
id,
|
||||||
(async () => {
|
(async () => {
|
||||||
if (this._system) await this._system.exit()
|
if (this._system) {
|
||||||
|
let target = null
|
||||||
|
if (params.target)
|
||||||
|
try {
|
||||||
|
target = ExtendedVersion.parse(params.target)
|
||||||
|
} catch (_) {
|
||||||
|
target = VersionRange.parse(params.target).normalize()
|
||||||
|
}
|
||||||
|
await this._system.exit(
|
||||||
|
makeEffects({
|
||||||
|
eventId: params.id,
|
||||||
|
}),
|
||||||
|
target,
|
||||||
|
)
|
||||||
|
this.shouldExit = true
|
||||||
|
}
|
||||||
})().then((result) => ({ result })),
|
})().then((result) => ({ result })),
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
.when(initType, async ({ id }) => {
|
case "init": {
|
||||||
|
const { id, params } = initType.parse(input)
|
||||||
return handleRpc(
|
return handleRpc(
|
||||||
id,
|
id,
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -335,22 +340,26 @@ export class RpcListener {
|
|||||||
const system = await this.getDependencies.system()
|
const system = await this.getDependencies.system()
|
||||||
this.callbacks = new CallbackHolder(
|
this.callbacks = new CallbackHolder(
|
||||||
makeEffects({
|
makeEffects({
|
||||||
procedureId: null,
|
eventId: params.id,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const callbacks = this.callbackHolderFor("containerInit")
|
const callbacks = this.callbacks.child("init")
|
||||||
await system.containerInit(
|
console.error("Initializing...")
|
||||||
|
await system.init(
|
||||||
makeEffects({
|
makeEffects({
|
||||||
procedureId: null,
|
eventId: params.id,
|
||||||
callbacks,
|
callbacks,
|
||||||
}),
|
}),
|
||||||
|
params.kind,
|
||||||
)
|
)
|
||||||
|
console.error("Initialization complete.")
|
||||||
this._system = system
|
this._system = system
|
||||||
}
|
}
|
||||||
})().then((result) => ({ result })),
|
})().then((result) => ({ result })),
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
.when(evalType, async ({ id, params }) => {
|
case "eval": {
|
||||||
|
const { id, params } = evalType.parse(input)
|
||||||
return handleRpc(
|
return handleRpc(
|
||||||
id,
|
id,
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -375,44 +384,31 @@ export class RpcListener {
|
|||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
.when(
|
default: {
|
||||||
shape({ id: idType, method: string }, ["id"]),
|
const { id, method } = z
|
||||||
({ id, method }) => ({
|
.object({ id: idType.optional(), method: z.string() })
|
||||||
|
.passthrough()
|
||||||
|
.parse(input)
|
||||||
|
return {
|
||||||
jsonrpc,
|
jsonrpc,
|
||||||
id,
|
id,
|
||||||
error: {
|
error: {
|
||||||
code: -32601,
|
code: -32601,
|
||||||
message: `Method not found`,
|
message: "Method not found",
|
||||||
data: {
|
data: {
|
||||||
details: method,
|
details: method,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
.defaultToLazy(() => {
|
|
||||||
console.warn(
|
|
||||||
`Couldn't parse the following input ${JSON.stringify(input)}`,
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
jsonrpc,
|
|
||||||
id: (input as any)?.id,
|
|
||||||
error: {
|
|
||||||
code: -32602,
|
|
||||||
message: "invalid params",
|
|
||||||
data: {
|
|
||||||
details: JSON.stringify(input),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private getResult(
|
private getResult(
|
||||||
procedure: typeof jsonPath._TYPE,
|
procedure: z.infer<typeof jsonPath>,
|
||||||
system: System,
|
system: System,
|
||||||
procedureId: string,
|
eventId: string,
|
||||||
timeout: number | undefined,
|
timeout: number | null | undefined,
|
||||||
input: any,
|
input: any,
|
||||||
) {
|
) {
|
||||||
const ensureResultTypeShape = (
|
const ensureResultTypeShape = (
|
||||||
@@ -420,9 +416,9 @@ export class RpcListener {
|
|||||||
): { result: any } => {
|
): { result: any } => {
|
||||||
return { result }
|
return { result }
|
||||||
}
|
}
|
||||||
const callbacks = this.callbackHolderFor(procedure)
|
const callbacks = this.callbacks?.child(procedure)
|
||||||
const effects = makeEffects({
|
const effects = makeEffects({
|
||||||
procedureId,
|
eventId,
|
||||||
callbacks,
|
callbacks,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -430,16 +426,6 @@ export class RpcListener {
|
|||||||
switch (procedure) {
|
switch (procedure) {
|
||||||
case "/backup/create":
|
case "/backup/create":
|
||||||
return system.createBackup(effects, timeout || null)
|
return system.createBackup(effects, timeout || null)
|
||||||
case "/backup/restore":
|
|
||||||
return system.restoreBackup(effects, timeout || null)
|
|
||||||
case "/packageInit":
|
|
||||||
return system.packageInit(effects, timeout || null)
|
|
||||||
case "/packageUninit":
|
|
||||||
return system.packageUninit(
|
|
||||||
effects,
|
|
||||||
string.optional().unsafeCast(input),
|
|
||||||
timeout || null,
|
|
||||||
)
|
|
||||||
default:
|
default:
|
||||||
const procedures = unNestPath(procedure)
|
const procedures = unNestPath(procedure)
|
||||||
switch (true) {
|
switch (true) {
|
||||||
@@ -447,6 +433,7 @@ export class RpcListener {
|
|||||||
return system.getActionInput(
|
return system.getActionInput(
|
||||||
effects,
|
effects,
|
||||||
procedures[2],
|
procedures[2],
|
||||||
|
input?.prefill ?? null,
|
||||||
timeout || null,
|
timeout || null,
|
||||||
)
|
)
|
||||||
case procedures[1] === "actions" && procedures[3] === "run":
|
case procedures[1] === "actions" && procedures[3] === "run":
|
||||||
@@ -458,30 +445,18 @@ export class RpcListener {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})().then(ensureResultTypeShape, (error) =>
|
})().then(ensureResultTypeShape, (error) => {
|
||||||
matches(error)
|
const errorSchema = z.object({
|
||||||
.when(
|
error: z.string(),
|
||||||
object(
|
code: z.number().default(0),
|
||||||
{
|
})
|
||||||
error: string,
|
const parsed = errorSchema.safeParse(error)
|
||||||
code: number,
|
if (parsed.success) {
|
||||||
},
|
return {
|
||||||
["code"],
|
error: { code: parsed.data.code, message: parsed.data.error },
|
||||||
{ code: 0 },
|
}
|
||||||
),
|
}
|
||||||
(error) => ({
|
return { error: { code: 0, message: String(error) } }
|
||||||
error: {
|
})
|
||||||
code: error.code,
|
|
||||||
message: error.error,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.defaultToLazy(() => ({
|
|
||||||
error: {
|
|
||||||
code: 0,
|
|
||||||
message: String(error),
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,26 +2,35 @@ import * as fs from "fs/promises"
|
|||||||
import * as cp from "child_process"
|
import * as cp from "child_process"
|
||||||
import { SubContainer, types as T } from "@start9labs/start-sdk"
|
import { SubContainer, types as T } from "@start9labs/start-sdk"
|
||||||
import { promisify } from "util"
|
import { promisify } from "util"
|
||||||
import { DockerProcedure, VolumeId } from "../../../Models/DockerProcedure"
|
import { DockerProcedure } from "../../../Models/DockerProcedure"
|
||||||
import { Volume } from "./matchVolume"
|
import { Volume } from "./matchVolume"
|
||||||
import {
|
import {
|
||||||
CommandOptions,
|
CommandOptions,
|
||||||
ExecOptions,
|
ExecOptions,
|
||||||
ExecSpawnable,
|
SubContainerOwned,
|
||||||
} from "@start9labs/start-sdk/package/lib/util/SubContainer"
|
} from "@start9labs/start-sdk/package/lib/util/SubContainer"
|
||||||
|
import { Mounts } from "@start9labs/start-sdk/package/lib/mainFn/Mounts"
|
||||||
|
import { Manifest } from "@start9labs/start-sdk/base/lib/osBindings"
|
||||||
|
import { BackupEffects } from "@start9labs/start-sdk/package/lib/backup/Backups"
|
||||||
|
import { Drop } from "@start9labs/start-sdk/package/lib/util"
|
||||||
|
import { SDKManifest } from "@start9labs/start-sdk/base/lib/types"
|
||||||
export const exec = promisify(cp.exec)
|
export const exec = promisify(cp.exec)
|
||||||
export const execFile = promisify(cp.execFile)
|
export const execFile = promisify(cp.execFile)
|
||||||
|
|
||||||
export class DockerProcedureContainer {
|
export class DockerProcedureContainer extends Drop {
|
||||||
private constructor(private readonly subcontainer: ExecSpawnable) {}
|
private constructor(
|
||||||
|
private readonly subcontainer: SubContainer<SDKManifest>,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
static async of(
|
static async of(
|
||||||
effects: T.Effects,
|
effects: T.Effects,
|
||||||
packageId: string,
|
packageId: string,
|
||||||
data: DockerProcedure,
|
data: DockerProcedure,
|
||||||
volumes: { [id: VolumeId]: Volume },
|
volumes: { [id: string]: Volume },
|
||||||
name: string,
|
name: string,
|
||||||
options: { subcontainer?: ExecSpawnable } = {},
|
options: { subcontainer?: SubContainer<SDKManifest> } = {},
|
||||||
) {
|
) {
|
||||||
const subcontainer =
|
const subcontainer =
|
||||||
options?.subcontainer ??
|
options?.subcontainer ??
|
||||||
@@ -38,12 +47,13 @@ export class DockerProcedureContainer {
|
|||||||
effects: T.Effects,
|
effects: T.Effects,
|
||||||
packageId: string,
|
packageId: string,
|
||||||
data: DockerProcedure,
|
data: DockerProcedure,
|
||||||
volumes: { [id: VolumeId]: Volume },
|
volumes: { [id: string]: Volume },
|
||||||
name: string,
|
name: string,
|
||||||
) {
|
) {
|
||||||
const subcontainer = await SubContainer.of(
|
const subcontainer = await SubContainerOwned.of(
|
||||||
effects,
|
effects as BackupEffects,
|
||||||
{ imageId: data.image },
|
{ imageId: data.image },
|
||||||
|
null,
|
||||||
name,
|
name,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,30 +64,33 @@ export class DockerProcedureContainer {
|
|||||||
? `${subcontainer.rootfs}${mounts[mount]}`
|
? `${subcontainer.rootfs}${mounts[mount]}`
|
||||||
: `${subcontainer.rootfs}/${mounts[mount]}`
|
: `${subcontainer.rootfs}/${mounts[mount]}`
|
||||||
await fs.mkdir(path, { recursive: true })
|
await fs.mkdir(path, { recursive: true })
|
||||||
const volumeMount = volumes[mount]
|
const volumeMount: Volume = volumes[mount]
|
||||||
if (volumeMount.type === "data") {
|
if (volumeMount.type === "data") {
|
||||||
await subcontainer.mount(
|
await subcontainer.mount(
|
||||||
{ type: "volume", id: mount, subpath: null, readonly: false },
|
Mounts.of().mountVolume({
|
||||||
mounts[mount],
|
volumeId: mount,
|
||||||
|
subpath: null,
|
||||||
|
mountpoint: mounts[mount],
|
||||||
|
readonly: false,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
} else if (volumeMount.type === "assets") {
|
} else if (volumeMount.type === "assets") {
|
||||||
await subcontainer.mount(
|
await subcontainer.mount(
|
||||||
{ type: "assets", subpath: mount },
|
Mounts.of().mountAssets({
|
||||||
mounts[mount],
|
subpath: mount,
|
||||||
|
mountpoint: mounts[mount],
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
} else if (volumeMount.type === "certificate") {
|
} else if (volumeMount.type === "certificate") {
|
||||||
|
const hostInfo = await effects.getHostInfo({
|
||||||
|
hostId: volumeMount["interface-id"],
|
||||||
|
})
|
||||||
const hostnames = [
|
const hostnames = [
|
||||||
`${packageId}.embassy`,
|
`${packageId}.embassy`,
|
||||||
...new Set(
|
...new Set(
|
||||||
Object.values(
|
Object.values(hostInfo?.bindings || {})
|
||||||
(
|
.flatMap((b) => b.addresses.available)
|
||||||
await effects.getHostInfo({
|
.map((h) => h.hostname),
|
||||||
hostId: volumeMount["interface-id"],
|
|
||||||
})
|
|
||||||
)?.hostnameInfo || {},
|
|
||||||
)
|
|
||||||
.flatMap((h) => h)
|
|
||||||
.flatMap((h) => (h.kind === "onion" ? [h.hostname.value] : [])),
|
|
||||||
).values(),
|
).values(),
|
||||||
]
|
]
|
||||||
const certChain = await effects.getSslCertificate({
|
const certChain = await effects.getSslCertificate({
|
||||||
@@ -95,21 +108,22 @@ export class DockerProcedureContainer {
|
|||||||
key,
|
key,
|
||||||
)
|
)
|
||||||
} else if (volumeMount.type === "pointer") {
|
} else if (volumeMount.type === "pointer") {
|
||||||
await effects
|
await effects.mount({
|
||||||
.mount({
|
location: path,
|
||||||
location: path,
|
target: {
|
||||||
target: {
|
packageId: volumeMount["package-id"],
|
||||||
packageId: volumeMount["package-id"],
|
subpath: volumeMount.path,
|
||||||
subpath: volumeMount.path,
|
readonly: volumeMount.readonly,
|
||||||
readonly: volumeMount.readonly,
|
volumeId: volumeMount["volume-id"],
|
||||||
volumeId: volumeMount["volume-id"],
|
idmap: [],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.catch(console.warn)
|
|
||||||
} else if (volumeMount.type === "backup") {
|
} else if (volumeMount.type === "backup") {
|
||||||
await subcontainer.mount(
|
await subcontainer.mount(
|
||||||
{ type: "backup", subpath: null },
|
Mounts.of().mountBackups({
|
||||||
mounts[mount],
|
subpath: null,
|
||||||
|
mountpoint: mounts[mount],
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +165,11 @@ export class DockerProcedureContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async spawn(commands: string[]): Promise<cp.ChildProcess> {
|
// async spawn(commands: string[]): Promise<cp.ChildProcess> {
|
||||||
return await this.subcontainer.spawn(commands)
|
// return await this.subcontainer.spawn(commands)
|
||||||
|
// }
|
||||||
|
|
||||||
|
onDrop(): void {
|
||||||
|
this.subcontainer.destroy?.()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,23 @@ import { Daemon } from "@start9labs/start-sdk/package/lib/mainFn/Daemon"
|
|||||||
import { Effects } from "../../../Models/Effects"
|
import { Effects } from "../../../Models/Effects"
|
||||||
import { off } from "node:process"
|
import { off } from "node:process"
|
||||||
import { CommandController } from "@start9labs/start-sdk/package/lib/mainFn/CommandController"
|
import { CommandController } from "@start9labs/start-sdk/package/lib/mainFn/CommandController"
|
||||||
|
import { SDKManifest } from "@start9labs/start-sdk/base/lib/types"
|
||||||
|
import { SubContainerRc } from "@start9labs/start-sdk/package/lib/util/SubContainer"
|
||||||
|
|
||||||
const EMBASSY_HEALTH_INTERVAL = 15 * 1000
|
const EMBASSY_HEALTH_INTERVAL = 15 * 1000
|
||||||
const EMBASSY_PROPERTIES_LOOP = 30 * 1000
|
|
||||||
/**
|
/**
|
||||||
* We wanted something to represent what the main loop is doing, and
|
* We wanted something to represent what the main loop is doing, and
|
||||||
* in this case it used to run the properties, health, and the docker/ js main.
|
* in this case it used to run the properties, health, and the docker/ js main.
|
||||||
* Also, this has an ability to clean itself up too if need be.
|
* Also, this has an ability to clean itself up too if need be.
|
||||||
*/
|
*/
|
||||||
export class MainLoop {
|
export class MainLoop {
|
||||||
|
private subcontainerRc?: SubContainerRc<SDKManifest>
|
||||||
get mainSubContainerHandle() {
|
get mainSubContainerHandle() {
|
||||||
return this.mainEvent?.daemon?.subContainerHandle
|
this.subcontainerRc =
|
||||||
|
this.subcontainerRc ??
|
||||||
|
this.mainEvent?.daemon?.subcontainerRc() ??
|
||||||
|
undefined
|
||||||
|
return this.subcontainerRc
|
||||||
}
|
}
|
||||||
private healthLoops?: {
|
private healthLoops?: {
|
||||||
name: string
|
name: string
|
||||||
@@ -24,7 +30,7 @@ export class MainLoop {
|
|||||||
}[]
|
}[]
|
||||||
|
|
||||||
private mainEvent?: {
|
private mainEvent?: {
|
||||||
daemon: Daemon
|
daemon: Daemon<SDKManifest>
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
@@ -55,28 +61,20 @@ export class MainLoop {
|
|||||||
if (jsMain) {
|
if (jsMain) {
|
||||||
throw new Error("Unreachable")
|
throw new Error("Unreachable")
|
||||||
}
|
}
|
||||||
const daemon = new Daemon(async () => {
|
const subcontainer = await DockerProcedureContainer.createSubContainer(
|
||||||
const subcontainer = await DockerProcedureContainer.createSubContainer(
|
effects,
|
||||||
effects,
|
this.system.manifest.id,
|
||||||
this.system.manifest.id,
|
this.system.manifest.main,
|
||||||
this.system.manifest.main,
|
this.system.manifest.volumes,
|
||||||
this.system.manifest.volumes,
|
`Main - ${currentCommand.join(" ")}`,
|
||||||
`Main - ${currentCommand.join(" ")}`,
|
)
|
||||||
)
|
const daemon = await Daemon.of()(this.effects, subcontainer, {
|
||||||
return CommandController.of()(
|
command: currentCommand,
|
||||||
this.effects,
|
runAsInit: true,
|
||||||
subcontainer,
|
env: {
|
||||||
currentCommand,
|
TINI_SUBREAPER: "true",
|
||||||
{
|
},
|
||||||
runAsInit: true,
|
sigtermTimeout: utils.inMs(this.system.manifest.main["sigterm-timeout"]),
|
||||||
env: {
|
|
||||||
TINI_SUBREAPER: "true",
|
|
||||||
},
|
|
||||||
sigtermTimeout: utils.inMs(
|
|
||||||
this.system.manifest.main["sigterm-timeout"],
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
daemon.start()
|
daemon.start()
|
||||||
@@ -121,6 +119,7 @@ export class MainLoop {
|
|||||||
? {
|
? {
|
||||||
preferredExternalPort: lanConf.external,
|
preferredExternalPort: lanConf.external,
|
||||||
alpn: { specified: ["http/1.1"] },
|
alpn: { specified: ["http/1.1"] },
|
||||||
|
addXForwardedHeaders: false,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
})
|
})
|
||||||
@@ -134,7 +133,7 @@ export class MainLoop {
|
|||||||
delete this.mainEvent
|
delete this.mainEvent
|
||||||
delete this.healthLoops
|
delete this.healthLoops
|
||||||
await main?.daemon
|
await main?.daemon
|
||||||
.stop()
|
.term()
|
||||||
.catch((e: unknown) => console.error(`Main loop error`, utils.asError(e)))
|
.catch((e: unknown) => console.error(`Main loop error`, utils.asError(e)))
|
||||||
this.effects.setMainStatus({ status: "stopped" })
|
this.effects.setMainStatus({ status: "stopped" })
|
||||||
if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval))
|
if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval))
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
export default {
|
||||||
|
nodes: {
|
||||||
|
type: "list",
|
||||||
|
subtype: "union",
|
||||||
|
name: "Lightning Nodes",
|
||||||
|
description: "List of Lightning Network node instances to manage",
|
||||||
|
range: "[1,*)",
|
||||||
|
default: ["lnd"],
|
||||||
|
spec: {
|
||||||
|
type: "string",
|
||||||
|
"display-as": "{{name}}",
|
||||||
|
"unique-by": "name",
|
||||||
|
name: "Node Implementation",
|
||||||
|
tag: {
|
||||||
|
id: "type",
|
||||||
|
name: "Type",
|
||||||
|
description:
|
||||||
|
"- LND: Lightning Network Daemon from Lightning Labs\n- CLN: Core Lightning from Blockstream\n",
|
||||||
|
"variant-names": {
|
||||||
|
lnd: "Lightning Network Daemon (LND)",
|
||||||
|
"c-lightning": "Core Lightning (CLN)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: "lnd",
|
||||||
|
variants: {
|
||||||
|
lnd: {
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
name: "Node Name",
|
||||||
|
description: "Name of this node in the list",
|
||||||
|
default: "StartOS LND",
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
"connection-settings": {
|
||||||
|
type: "union",
|
||||||
|
name: "Connection Settings",
|
||||||
|
description: "The Lightning Network Daemon node to connect to.",
|
||||||
|
tag: {
|
||||||
|
id: "type",
|
||||||
|
name: "Type",
|
||||||
|
description:
|
||||||
|
"- Internal: The Lightning Network Daemon service installed to your StartOS server.\n- External: A Lightning Network Daemon instance running on a remote device (advanced).\n",
|
||||||
|
"variant-names": {
|
||||||
|
internal: "Internal",
|
||||||
|
external: "External",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: "internal",
|
||||||
|
variants: {
|
||||||
|
internal: {},
|
||||||
|
external: {
|
||||||
|
address: {
|
||||||
|
type: "string",
|
||||||
|
name: "Public Address",
|
||||||
|
description:
|
||||||
|
"The public address of your LND REST server\nNOTE: RTL does not support a .onion URL here\n",
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
"rest-port": {
|
||||||
|
type: "number",
|
||||||
|
name: "REST Port",
|
||||||
|
description:
|
||||||
|
"The port that your Lightning Network Daemon REST server is bound to",
|
||||||
|
nullable: false,
|
||||||
|
range: "[0,65535]",
|
||||||
|
integral: true,
|
||||||
|
default: 8080,
|
||||||
|
},
|
||||||
|
macaroon: {
|
||||||
|
type: "string",
|
||||||
|
name: "Macaroon",
|
||||||
|
description:
|
||||||
|
'Your admin.macaroon file, Base64URL encoded. This is the same as the value after "macaroon=" in your lndconnect URL.',
|
||||||
|
nullable: false,
|
||||||
|
masked: true,
|
||||||
|
pattern: "[=A-Za-z0-9_-]+",
|
||||||
|
"pattern-description":
|
||||||
|
"Macaroon must be encoded in Base64URL format (only A-Z, a-z, 0-9, _, - and = allowed)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"c-lightning": {
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
name: "Node Name",
|
||||||
|
description: "Name of this node in the list",
|
||||||
|
default: "StartOS CLN",
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
"connection-settings": {
|
||||||
|
type: "union",
|
||||||
|
name: "Connection Settings",
|
||||||
|
description: "The Core Lightning (CLN) node to connect to.",
|
||||||
|
tag: {
|
||||||
|
id: "type",
|
||||||
|
name: "Type",
|
||||||
|
description:
|
||||||
|
"- Internal: The Core Lightning (CLN) service installed to your StartOS server.\n- External: A Core Lightning (CLN) instance running on a remote device (advanced).\n",
|
||||||
|
"variant-names": {
|
||||||
|
internal: "Internal",
|
||||||
|
external: "External",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: "internal",
|
||||||
|
variants: {
|
||||||
|
internal: {},
|
||||||
|
external: {
|
||||||
|
address: {
|
||||||
|
type: "string",
|
||||||
|
name: "Public Address",
|
||||||
|
description:
|
||||||
|
"The public address of your CLNRest server\nNOTE: RTL does not support a .onion URL here\n",
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
"rest-port": {
|
||||||
|
type: "number",
|
||||||
|
name: "CLNRest Port",
|
||||||
|
description: "The port that your CLNRest server is bound to",
|
||||||
|
nullable: false,
|
||||||
|
range: "[0,65535]",
|
||||||
|
integral: true,
|
||||||
|
default: 3010,
|
||||||
|
},
|
||||||
|
macaroon: {
|
||||||
|
type: "string",
|
||||||
|
name: "Rune",
|
||||||
|
description:
|
||||||
|
"Your CLNRest unrestricted Rune, Base64URL encoded.",
|
||||||
|
nullable: false,
|
||||||
|
masked: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: "string",
|
||||||
|
name: "Password",
|
||||||
|
description: "The password for your Ride the Lightning dashboard",
|
||||||
|
nullable: false,
|
||||||
|
copyable: true,
|
||||||
|
masked: true,
|
||||||
|
default: {
|
||||||
|
charset: "a-z,A-Z,0-9",
|
||||||
|
len: 22,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,5 +1,30 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`transformConfigSpec transformConfigSpec(RTL) 1`] = `
|
||||||
|
{
|
||||||
|
"password": {
|
||||||
|
"default": {
|
||||||
|
"charset": "a-z,A-Z,0-9",
|
||||||
|
"len": 22,
|
||||||
|
},
|
||||||
|
"description": "The password for your Ride the Lightning dashboard",
|
||||||
|
"disabled": false,
|
||||||
|
"generate": null,
|
||||||
|
"immutable": false,
|
||||||
|
"inputmode": "text",
|
||||||
|
"masked": true,
|
||||||
|
"maxLength": null,
|
||||||
|
"minLength": null,
|
||||||
|
"name": "Password",
|
||||||
|
"patterns": [],
|
||||||
|
"placeholder": null,
|
||||||
|
"required": true,
|
||||||
|
"type": "text",
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`transformConfigSpec transformConfigSpec(bitcoind) 1`] = `
|
exports[`transformConfigSpec transformConfigSpec(bitcoind) 1`] = `
|
||||||
{
|
{
|
||||||
"advanced": {
|
"advanced": {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
ExtendedVersion,
|
ExtendedVersion,
|
||||||
|
FileHelper,
|
||||||
|
getDataVersion,
|
||||||
|
overlaps,
|
||||||
types as T,
|
types as T,
|
||||||
utils,
|
utils,
|
||||||
VersionRange,
|
VersionRange,
|
||||||
@@ -12,26 +15,11 @@ import { System } from "../../../Interfaces/System"
|
|||||||
import { matchManifest, Manifest } from "./matchManifest"
|
import { matchManifest, Manifest } from "./matchManifest"
|
||||||
import * as childProcess from "node:child_process"
|
import * as childProcess from "node:child_process"
|
||||||
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
||||||
|
import { DockerProcedure } from "../../../Models/DockerProcedure"
|
||||||
import { promisify } from "node:util"
|
import { promisify } from "node:util"
|
||||||
import * as U from "./oldEmbassyTypes"
|
import * as U from "./oldEmbassyTypes"
|
||||||
import { MainLoop } from "./MainLoop"
|
import { MainLoop } from "./MainLoop"
|
||||||
import {
|
import { z } from "@start9labs/start-sdk"
|
||||||
matches,
|
|
||||||
boolean,
|
|
||||||
dictionary,
|
|
||||||
literal,
|
|
||||||
literals,
|
|
||||||
object,
|
|
||||||
string,
|
|
||||||
unknown,
|
|
||||||
any,
|
|
||||||
tuple,
|
|
||||||
number,
|
|
||||||
anyOf,
|
|
||||||
deferred,
|
|
||||||
Parser,
|
|
||||||
array,
|
|
||||||
} from "ts-matches"
|
|
||||||
import { AddSslOptions } from "@start9labs/start-sdk/base/lib/osBindings"
|
import { AddSslOptions } from "@start9labs/start-sdk/base/lib/osBindings"
|
||||||
import {
|
import {
|
||||||
BindOptionsByProtocol,
|
BindOptionsByProtocol,
|
||||||
@@ -47,27 +35,48 @@ import {
|
|||||||
transformOldConfigToNew,
|
transformOldConfigToNew,
|
||||||
} from "./transformConfigSpec"
|
} from "./transformConfigSpec"
|
||||||
import { partialDiff } from "@start9labs/start-sdk/base/lib/util"
|
import { partialDiff } from "@start9labs/start-sdk/base/lib/util"
|
||||||
|
import { Volume } from "@start9labs/start-sdk/package/lib/util/Volume"
|
||||||
|
|
||||||
type Optional<A> = A | undefined | null
|
type Optional<A> = A | undefined | null
|
||||||
function todo(): never {
|
function todo(): never {
|
||||||
throw new Error("Not implemented")
|
throw new Error("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local type for procedure values from the manifest.
|
||||||
|
* The manifest's zod schemas use ZodTypeAny casts that produce `unknown` in zod v4.
|
||||||
|
* This type restores the expected shape for type-safe property access.
|
||||||
|
*/
|
||||||
|
type Procedure =
|
||||||
|
| (DockerProcedure & { type: "docker" })
|
||||||
|
| { type: "script"; args: unknown[] | null }
|
||||||
|
|
||||||
const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json"
|
const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json"
|
||||||
export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js"
|
export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js"
|
||||||
const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig" as utils.StorePath
|
|
||||||
const EMBASSY_DEPENDS_ON_PATH_PREFIX = "/embassyDependsOn" as utils.StorePath
|
|
||||||
|
|
||||||
const matchResult = object({
|
const configFile = FileHelper.json(
|
||||||
result: any,
|
{
|
||||||
|
base: new Volume("embassy"),
|
||||||
|
subpath: "config.json",
|
||||||
|
},
|
||||||
|
z.any(),
|
||||||
|
)
|
||||||
|
const dependsOnFile = FileHelper.json(
|
||||||
|
{
|
||||||
|
base: new Volume("embassy"),
|
||||||
|
subpath: "dependsOn.json",
|
||||||
|
},
|
||||||
|
z.record(z.string(), z.array(z.string())),
|
||||||
|
)
|
||||||
|
|
||||||
|
const matchResult = z.object({
|
||||||
|
result: z.any(),
|
||||||
})
|
})
|
||||||
const matchError = object({
|
const matchError = z.object({
|
||||||
error: string,
|
error: z.string(),
|
||||||
})
|
})
|
||||||
const matchErrorCode = object<{
|
const matchErrorCode = z.object({
|
||||||
"error-code": [number, string] | readonly [number, string]
|
"error-code": z.tuple([z.number(), z.string()]),
|
||||||
}>({
|
|
||||||
"error-code": tuple(number, string),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const assertNever = (
|
const assertNever = (
|
||||||
@@ -79,62 +88,68 @@ const assertNever = (
|
|||||||
/**
|
/**
|
||||||
Should be changing the type for specific properties, and this is mostly a transformation for the old return types to the newer one.
|
Should be changing the type for specific properties, and this is mostly a transformation for the old return types to the newer one.
|
||||||
*/
|
*/
|
||||||
|
function isMatchResult(a: unknown): a is z.infer<typeof matchResult> {
|
||||||
|
return matchResult.safeParse(a).success
|
||||||
|
}
|
||||||
|
function isMatchError(a: unknown): a is z.infer<typeof matchError> {
|
||||||
|
return matchError.safeParse(a).success
|
||||||
|
}
|
||||||
|
function isMatchErrorCode(a: unknown): a is z.infer<typeof matchErrorCode> {
|
||||||
|
return matchErrorCode.safeParse(a).success
|
||||||
|
}
|
||||||
const fromReturnType = <A>(a: U.ResultType<A>): A => {
|
const fromReturnType = <A>(a: U.ResultType<A>): A => {
|
||||||
if (matchResult.test(a)) {
|
if (isMatchResult(a)) {
|
||||||
return a.result
|
return a.result
|
||||||
}
|
}
|
||||||
if (matchError.test(a)) {
|
if (isMatchError(a)) {
|
||||||
console.info({ passedErrorStack: new Error().stack, error: a.error })
|
console.info({ passedErrorStack: new Error().stack, error: a.error })
|
||||||
throw { error: a.error }
|
throw { error: a.error }
|
||||||
}
|
}
|
||||||
if (matchErrorCode.test(a)) {
|
if (isMatchErrorCode(a)) {
|
||||||
const [code, message] = a["error-code"]
|
const [code, message] = a["error-code"]
|
||||||
throw { error: message, code }
|
throw { error: message, code }
|
||||||
}
|
}
|
||||||
return assertNever(a)
|
return assertNever(a as never)
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchSetResult = object(
|
const matchSetResult = z.object({
|
||||||
{
|
"depends-on": z.record(z.string(), z.array(z.string())).nullable().optional(),
|
||||||
"depends-on": dictionary([string, array(string)]),
|
dependsOn: z.record(z.string(), z.array(z.string())).nullable().optional(),
|
||||||
dependsOn: dictionary([string, array(string)]),
|
signal: z.enum([
|
||||||
signal: literals(
|
"SIGTERM",
|
||||||
"SIGTERM",
|
"SIGHUP",
|
||||||
"SIGHUP",
|
"SIGINT",
|
||||||
"SIGINT",
|
"SIGQUIT",
|
||||||
"SIGQUIT",
|
"SIGILL",
|
||||||
"SIGILL",
|
"SIGTRAP",
|
||||||
"SIGTRAP",
|
"SIGABRT",
|
||||||
"SIGABRT",
|
"SIGBUS",
|
||||||
"SIGBUS",
|
"SIGFPE",
|
||||||
"SIGFPE",
|
"SIGKILL",
|
||||||
"SIGKILL",
|
"SIGUSR1",
|
||||||
"SIGUSR1",
|
"SIGSEGV",
|
||||||
"SIGSEGV",
|
"SIGUSR2",
|
||||||
"SIGUSR2",
|
"SIGPIPE",
|
||||||
"SIGPIPE",
|
"SIGALRM",
|
||||||
"SIGALRM",
|
"SIGSTKFLT",
|
||||||
"SIGSTKFLT",
|
"SIGCHLD",
|
||||||
"SIGCHLD",
|
"SIGCONT",
|
||||||
"SIGCONT",
|
"SIGSTOP",
|
||||||
"SIGSTOP",
|
"SIGTSTP",
|
||||||
"SIGTSTP",
|
"SIGTTIN",
|
||||||
"SIGTTIN",
|
"SIGTTOU",
|
||||||
"SIGTTOU",
|
"SIGURG",
|
||||||
"SIGURG",
|
"SIGXCPU",
|
||||||
"SIGXCPU",
|
"SIGXFSZ",
|
||||||
"SIGXFSZ",
|
"SIGVTALRM",
|
||||||
"SIGVTALRM",
|
"SIGPROF",
|
||||||
"SIGPROF",
|
"SIGWINCH",
|
||||||
"SIGWINCH",
|
"SIGIO",
|
||||||
"SIGIO",
|
"SIGPWR",
|
||||||
"SIGPWR",
|
"SIGSYS",
|
||||||
"SIGSYS",
|
"SIGINFO",
|
||||||
"SIGINFO",
|
]),
|
||||||
),
|
})
|
||||||
},
|
|
||||||
["depends-on", "dependsOn"],
|
|
||||||
)
|
|
||||||
|
|
||||||
type OldGetConfigRes = {
|
type OldGetConfigRes = {
|
||||||
config?: null | Record<string, unknown>
|
config?: null | Record<string, unknown>
|
||||||
@@ -174,14 +189,14 @@ export type PackagePropertiesV2 = {
|
|||||||
}
|
}
|
||||||
export type PackagePropertyString = {
|
export type PackagePropertyString = {
|
||||||
type: "string"
|
type: "string"
|
||||||
description?: string
|
description?: string | null
|
||||||
value: string
|
value: string
|
||||||
/** Let's the ui make this copyable button */
|
/** Let's the ui make this copyable button */
|
||||||
copyable?: boolean
|
copyable?: boolean | null
|
||||||
/** Let the ui create a qr for this field */
|
/** Let the ui create a qr for this field */
|
||||||
qr?: boolean
|
qr?: boolean | null
|
||||||
/** Hiding the value unless toggled off for field */
|
/** Hiding the value unless toggled off for field */
|
||||||
masked?: boolean
|
masked?: boolean | null
|
||||||
}
|
}
|
||||||
export type PackagePropertyObject = {
|
export type PackagePropertyObject = {
|
||||||
value: PackagePropertiesV2
|
value: PackagePropertiesV2
|
||||||
@@ -215,36 +230,29 @@ const asProperty = (x: PackagePropertiesV2): PropertiesReturn =>
|
|||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
Object.entries(x).map(([key, value]) => [key, asProperty_(value)]),
|
Object.entries(x).map(([key, value]) => [key, asProperty_(value)]),
|
||||||
)
|
)
|
||||||
const [matchPackageProperties, setMatchPackageProperties] =
|
const matchPackagePropertyObject: z.ZodType<PackagePropertyObject> = z.object({
|
||||||
deferred<PackagePropertiesV2>()
|
value: z.lazy(() => matchPackageProperties),
|
||||||
const matchPackagePropertyObject: Parser<unknown, PackagePropertyObject> =
|
type: z.literal("object"),
|
||||||
object({
|
description: z.string(),
|
||||||
value: matchPackageProperties,
|
})
|
||||||
type: literal("object"),
|
|
||||||
description: string,
|
|
||||||
})
|
|
||||||
|
|
||||||
const matchPackagePropertyString: Parser<unknown, PackagePropertyString> =
|
const matchPackagePropertyString: z.ZodType<PackagePropertyString> = z.object({
|
||||||
object(
|
type: z.literal("string"),
|
||||||
{
|
description: z.string().nullable().optional(),
|
||||||
type: literal("string"),
|
value: z.string(),
|
||||||
description: string,
|
copyable: z.boolean().nullable().optional(),
|
||||||
value: string,
|
qr: z.boolean().nullable().optional(),
|
||||||
copyable: boolean,
|
masked: z.boolean().nullable().optional(),
|
||||||
qr: boolean,
|
})
|
||||||
masked: boolean,
|
const matchPackageProperties: z.ZodType<PackagePropertiesV2> = z.lazy(() =>
|
||||||
},
|
z.record(
|
||||||
["copyable", "description", "qr", "masked"],
|
z.string(),
|
||||||
)
|
z.union([matchPackagePropertyObject, matchPackagePropertyString]),
|
||||||
setMatchPackageProperties(
|
),
|
||||||
dictionary([
|
|
||||||
string,
|
|
||||||
anyOf(matchPackagePropertyObject, matchPackagePropertyString),
|
|
||||||
]),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const matchProperties = object({
|
const matchProperties = z.object({
|
||||||
version: literal(2),
|
version: z.literal(2),
|
||||||
data: matchPackageProperties,
|
data: matchPackageProperties,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -273,8 +281,8 @@ function convertProperties(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_REGISTRY = "https://registry.start9.com"
|
|
||||||
export class SystemForEmbassy implements System {
|
export class SystemForEmbassy implements System {
|
||||||
|
private version: ExtendedVersion
|
||||||
currentRunning: MainLoop | undefined
|
currentRunning: MainLoop | undefined
|
||||||
static async of(manifestLocation: string = MANIFEST_LOCATION) {
|
static async of(manifestLocation: string = MANIFEST_LOCATION) {
|
||||||
const moduleCode = await import(EMBASSY_JS_LOCATION)
|
const moduleCode = await import(EMBASSY_JS_LOCATION)
|
||||||
@@ -288,7 +296,7 @@ export class SystemForEmbassy implements System {
|
|||||||
})
|
})
|
||||||
const manifestData = await fs.readFile(manifestLocation, "utf-8")
|
const manifestData = await fs.readFile(manifestLocation, "utf-8")
|
||||||
return new SystemForEmbassy(
|
return new SystemForEmbassy(
|
||||||
matchManifest.unsafeCast(JSON.parse(manifestData)),
|
matchManifest.parse(JSON.parse(manifestData)),
|
||||||
moduleCode,
|
moduleCode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -296,11 +304,41 @@ export class SystemForEmbassy implements System {
|
|||||||
constructor(
|
constructor(
|
||||||
readonly manifest: Manifest,
|
readonly manifest: Manifest,
|
||||||
readonly moduleCode: Partial<U.ExpectedExports>,
|
readonly moduleCode: Partial<U.ExpectedExports>,
|
||||||
) {}
|
) {
|
||||||
|
this.version = ExtendedVersion.parseEmver(manifest.version)
|
||||||
|
if (
|
||||||
|
this.manifest.id === "bitcoind" &&
|
||||||
|
this.manifest.title.toLowerCase().includes("knots")
|
||||||
|
)
|
||||||
|
this.version.flavor = "knots"
|
||||||
|
|
||||||
async containerInit(effects: Effects): Promise<void> {
|
if (
|
||||||
|
this.manifest.id === "lnd" ||
|
||||||
|
this.manifest.id === "ride-the-lightning" ||
|
||||||
|
this.manifest.id === "datum"
|
||||||
|
) {
|
||||||
|
this.version.upstream.prerelease = ["beta"]
|
||||||
|
} else if (
|
||||||
|
this.manifest.id === "lightning-terminal" ||
|
||||||
|
this.manifest.id === "robosats"
|
||||||
|
) {
|
||||||
|
this.version.upstream.prerelease = ["alpha"]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.manifest.id === "nostr") {
|
||||||
|
this.manifest.id = "nostr-rs-relay"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(
|
||||||
|
effects: Effects,
|
||||||
|
kind: "install" | "update" | "restore" | null,
|
||||||
|
): Promise<void> {
|
||||||
|
if (kind === "restore") {
|
||||||
|
await this.restoreBackup(effects, null)
|
||||||
|
}
|
||||||
for (let depId in this.manifest.dependencies) {
|
for (let depId in this.manifest.dependencies) {
|
||||||
if (this.manifest.dependencies[depId].config) {
|
if (this.manifest.dependencies[depId]?.config) {
|
||||||
await this.dependenciesAutoconfig(effects, depId, null)
|
await this.dependenciesAutoconfig(effects, depId, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,6 +346,9 @@ export class SystemForEmbassy implements System {
|
|||||||
await this.exportActions(effects)
|
await this.exportActions(effects)
|
||||||
await this.exportNetwork(effects)
|
await this.exportNetwork(effects)
|
||||||
await this.containerSetDependencies(effects)
|
await this.containerSetDependencies(effects)
|
||||||
|
if (kind === "install" || kind === "update") {
|
||||||
|
await this.packageInit(effects, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async containerSetDependencies(effects: T.Effects) {
|
async containerSetDependencies(effects: T.Effects) {
|
||||||
const oldDeps: Record<string, string[]> = Object.fromEntries(
|
const oldDeps: Record<string, string[]> = Object.fromEntries(
|
||||||
@@ -341,22 +382,31 @@ export class SystemForEmbassy implements System {
|
|||||||
delete this.currentRunning
|
delete this.currentRunning
|
||||||
if (currentRunning) {
|
if (currentRunning) {
|
||||||
await currentRunning.clean({
|
await currentRunning.clean({
|
||||||
timeout: fromDuration(this.manifest.main["sigterm-timeout"] || "30s"),
|
timeout: fromDuration(
|
||||||
|
(this.manifest.main["sigterm-timeout"] as any) || "30s",
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async packageInit(effects: Effects, timeoutMs: number | null): Promise<void> {
|
async packageInit(effects: Effects, timeoutMs: number | null): Promise<void> {
|
||||||
const previousVersion = await effects.getDataVersion()
|
const previousVersion = await getDataVersion(effects)
|
||||||
if (previousVersion) {
|
if (previousVersion) {
|
||||||
if (
|
const migrationRes = await this.migration(
|
||||||
(await this.migration(effects, { from: previousVersion }, timeoutMs))
|
effects,
|
||||||
.configured
|
{ from: previousVersion },
|
||||||
) {
|
timeoutMs,
|
||||||
await effects.action.clearRequests({ only: ["needs-config"] })
|
)
|
||||||
|
if (migrationRes) {
|
||||||
|
if (migrationRes.configured)
|
||||||
|
await effects.action.clearTasks({ only: ["needs-config"] })
|
||||||
|
await configFile.write(
|
||||||
|
effects,
|
||||||
|
await this.getConfig(effects, timeoutMs),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else if (this.manifest.config) {
|
} else if (this.manifest.config) {
|
||||||
await effects.action.request({
|
await effects.action.createTask({
|
||||||
packageId: this.manifest.id,
|
packageId: this.manifest.id,
|
||||||
actionId: "config",
|
actionId: "config",
|
||||||
severity: "critical",
|
severity: "critical",
|
||||||
@@ -364,9 +414,11 @@ export class SystemForEmbassy implements System {
|
|||||||
reason: "This service must be configured before it can be run",
|
reason: "This service must be configured before it can be run",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await effects.setDataVersion({
|
await effects.setDataVersion({
|
||||||
version: ExtendedVersion.parseEmver(this.manifest.version).toString(),
|
version: this.version.toString(),
|
||||||
})
|
})
|
||||||
|
// @FullMetal: package hacks go here
|
||||||
}
|
}
|
||||||
async exportNetwork(effects: Effects) {
|
async exportNetwork(effects: Effects) {
|
||||||
for (const [id, interfaceValue] of Object.entries(
|
for (const [id, interfaceValue] of Object.entries(
|
||||||
@@ -403,6 +455,7 @@ export class SystemForEmbassy implements System {
|
|||||||
addSsl = {
|
addSsl = {
|
||||||
preferredExternalPort: lanPortNum,
|
preferredExternalPort: lanPortNum,
|
||||||
alpn: { specified: [] },
|
alpn: { specified: [] },
|
||||||
|
addXForwardedHeaders: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
@@ -441,7 +494,7 @@ export class SystemForEmbassy implements System {
|
|||||||
masked: false,
|
masked: false,
|
||||||
path: "",
|
path: "",
|
||||||
schemeOverride: null,
|
schemeOverride: null,
|
||||||
search: {},
|
query: {},
|
||||||
username: null,
|
username: null,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@@ -452,17 +505,23 @@ export class SystemForEmbassy implements System {
|
|||||||
async getActionInput(
|
async getActionInput(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
actionId: string,
|
actionId: string,
|
||||||
|
_prefill: Record<string, unknown> | null,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<T.ActionInput | null> {
|
): Promise<T.ActionInput | null> {
|
||||||
if (actionId === "config") {
|
if (actionId === "config") {
|
||||||
const config = await this.getConfig(effects, timeoutMs)
|
const config = await this.getConfig(effects, timeoutMs)
|
||||||
return { spec: config.spec, value: config.config }
|
return {
|
||||||
|
eventId: effects.eventId!,
|
||||||
|
spec: config.spec,
|
||||||
|
value: config.config,
|
||||||
|
}
|
||||||
} else if (actionId === "properties") {
|
} else if (actionId === "properties") {
|
||||||
return null
|
return null
|
||||||
} else {
|
} else {
|
||||||
const oldSpec = this.manifest.actions?.[actionId]?.["input-spec"]
|
const oldSpec = this.manifest.actions?.[actionId]?.["input-spec"]
|
||||||
if (!oldSpec) return null
|
if (!oldSpec) return null
|
||||||
return {
|
return {
|
||||||
|
eventId: effects.eventId!,
|
||||||
spec: transformConfigSpec(oldSpec as OldConfigSpec),
|
spec: transformConfigSpec(oldSpec as OldConfigSpec),
|
||||||
value: null,
|
value: null,
|
||||||
}
|
}
|
||||||
@@ -543,14 +602,14 @@ export class SystemForEmbassy implements System {
|
|||||||
}
|
}
|
||||||
await effects.action.clear({ except: Object.keys(actions) })
|
await effects.action.clear({ except: Object.keys(actions) })
|
||||||
}
|
}
|
||||||
async packageUninit(
|
async uninit(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
nextVersion: Optional<string>,
|
target: ExtendedVersion | VersionRange | null,
|
||||||
timeoutMs: number | null,
|
timeoutMs?: number | null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.currentRunning?.clean({ timeout: timeoutMs ?? undefined })
|
await this.currentRunning?.clean({ timeout: timeoutMs ?? undefined })
|
||||||
if (nextVersion) {
|
if (target) {
|
||||||
await this.migration(effects, { to: nextVersion }, timeoutMs)
|
await this.migration(effects, { to: target }, timeoutMs ?? null)
|
||||||
}
|
}
|
||||||
await effects.setMainStatus({ status: "stopped" })
|
await effects.setMainStatus({ status: "stopped" })
|
||||||
}
|
}
|
||||||
@@ -559,7 +618,7 @@ export class SystemForEmbassy implements System {
|
|||||||
effects: Effects,
|
effects: Effects,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const backup = this.manifest.backup.create
|
const backup = this.manifest.backup.create as Procedure
|
||||||
if (backup.type === "docker") {
|
if (backup.type === "docker") {
|
||||||
const commands = [backup.entrypoint, ...backup.args]
|
const commands = [backup.entrypoint, ...backup.args]
|
||||||
const container = await DockerProcedureContainer.of(
|
const container = await DockerProcedureContainer.of(
|
||||||
@@ -577,12 +636,22 @@ export class SystemForEmbassy implements System {
|
|||||||
const moduleCode = await this.moduleCode
|
const moduleCode = await this.moduleCode
|
||||||
await moduleCode.createBackup?.(polyfillEffects(effects, this.manifest))
|
await moduleCode.createBackup?.(polyfillEffects(effects, this.manifest))
|
||||||
}
|
}
|
||||||
|
const dataVersion = await effects.getDataVersion()
|
||||||
|
if (dataVersion)
|
||||||
|
await fs.writeFile("/media/startos/backup/dataVersion.txt", dataVersion, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
async restoreBackup(
|
async restoreBackup(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const restoreBackup = this.manifest.backup.restore
|
const store = await fs
|
||||||
|
.readFile("/media/startos/backup/store.json", {
|
||||||
|
encoding: "utf-8",
|
||||||
|
})
|
||||||
|
.catch((_) => null)
|
||||||
|
const restoreBackup = this.manifest.backup.restore as Procedure
|
||||||
if (restoreBackup.type === "docker") {
|
if (restoreBackup.type === "docker") {
|
||||||
const commands = [restoreBackup.entrypoint, ...restoreBackup.args]
|
const commands = [restoreBackup.entrypoint, ...restoreBackup.args]
|
||||||
const container = await DockerProcedureContainer.of(
|
const container = await DockerProcedureContainer.of(
|
||||||
@@ -600,6 +669,13 @@ export class SystemForEmbassy implements System {
|
|||||||
const moduleCode = await this.moduleCode
|
const moduleCode = await this.moduleCode
|
||||||
await moduleCode.restoreBackup?.(polyfillEffects(effects, this.manifest))
|
await moduleCode.restoreBackup?.(polyfillEffects(effects, this.manifest))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dataVersion = await fs
|
||||||
|
.readFile("/media/startos/backup/dataVersion.txt", {
|
||||||
|
encoding: "utf-8",
|
||||||
|
})
|
||||||
|
.catch((_) => null)
|
||||||
|
if (dataVersion) await effects.setDataVersion({ version: dataVersion })
|
||||||
}
|
}
|
||||||
async getConfig(effects: Effects, timeoutMs: number | null) {
|
async getConfig(effects: Effects, timeoutMs: number | null) {
|
||||||
return this.getConfigUncleaned(effects, timeoutMs).then(convertToNewConfig)
|
return this.getConfigUncleaned(effects, timeoutMs).then(convertToNewConfig)
|
||||||
@@ -608,7 +684,7 @@ export class SystemForEmbassy implements System {
|
|||||||
effects: Effects,
|
effects: Effects,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<OldGetConfigRes> {
|
): Promise<OldGetConfigRes> {
|
||||||
const config = this.manifest.config?.get
|
const config = this.manifest.config?.get as Procedure | undefined
|
||||||
if (!config) return { spec: {} }
|
if (!config) return { spec: {} }
|
||||||
if (config.type === "docker") {
|
if (config.type === "docker") {
|
||||||
const commands = [config.entrypoint, ...config.args]
|
const commands = [config.entrypoint, ...config.args]
|
||||||
@@ -649,11 +725,8 @@ export class SystemForEmbassy implements System {
|
|||||||
structuredClone(newConfigWithoutPointers as Record<string, unknown>),
|
structuredClone(newConfigWithoutPointers as Record<string, unknown>),
|
||||||
)
|
)
|
||||||
await updateConfig(effects, this.manifest, spec, newConfig)
|
await updateConfig(effects, this.manifest, spec, newConfig)
|
||||||
await effects.store.set({
|
await configFile.write(effects, newConfig)
|
||||||
path: EMBASSY_POINTER_PATH_PREFIX,
|
const setConfigValue = this.manifest.config?.set as Procedure | undefined
|
||||||
value: newConfig,
|
|
||||||
})
|
|
||||||
const setConfigValue = this.manifest.config?.set
|
|
||||||
if (!setConfigValue) return
|
if (!setConfigValue) return
|
||||||
if (setConfigValue.type === "docker") {
|
if (setConfigValue.type === "docker") {
|
||||||
const commands = [
|
const commands = [
|
||||||
@@ -668,7 +741,7 @@ export class SystemForEmbassy implements System {
|
|||||||
this.manifest.volumes,
|
this.manifest.volumes,
|
||||||
`Set Config - ${commands.join(" ")}`,
|
`Set Config - ${commands.join(" ")}`,
|
||||||
)
|
)
|
||||||
const answer = matchSetResult.unsafeCast(
|
const answer = matchSetResult.parse(
|
||||||
JSON.parse(
|
JSON.parse(
|
||||||
(await container.execFail(commands, timeoutMs)).stdout.toString(),
|
(await container.execFail(commands, timeoutMs)).stdout.toString(),
|
||||||
),
|
),
|
||||||
@@ -681,7 +754,7 @@ export class SystemForEmbassy implements System {
|
|||||||
const method = moduleCode.setConfig
|
const method = moduleCode.setConfig
|
||||||
if (!method) throw new Error("Expecting that the method setConfig exists")
|
if (!method) throw new Error("Expecting that the method setConfig exists")
|
||||||
|
|
||||||
const answer = matchSetResult.unsafeCast(
|
const answer = matchSetResult.parse(
|
||||||
await method(
|
await method(
|
||||||
polyfillEffects(effects, this.manifest),
|
polyfillEffects(effects, this.manifest),
|
||||||
newConfig as U.Config,
|
newConfig as U.Config,
|
||||||
@@ -706,15 +779,15 @@ export class SystemForEmbassy implements System {
|
|||||||
rawDepends: { [x: string]: readonly string[] },
|
rawDepends: { [x: string]: readonly string[] },
|
||||||
configuring: boolean,
|
configuring: boolean,
|
||||||
) {
|
) {
|
||||||
const storedDependsOn = (await effects.store.get({
|
const storedDependsOn = await dependsOnFile.read().once()
|
||||||
packageId: this.manifest.id,
|
|
||||||
path: EMBASSY_DEPENDS_ON_PATH_PREFIX,
|
|
||||||
})) as Record<string, readonly string[]>
|
|
||||||
|
|
||||||
const requiredDeps = {
|
const requiredDeps = {
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
Object.entries(this.manifest.dependencies || {})
|
Object.entries(this.manifest.dependencies ?? {})
|
||||||
?.filter((x) => x[1].requirement.type === "required")
|
.filter(
|
||||||
|
([k, v]) =>
|
||||||
|
(v?.requirement as { type: string } | undefined)?.type ===
|
||||||
|
"required",
|
||||||
|
)
|
||||||
.map((x) => [x[0], []]) || [],
|
.map((x) => [x[0], []]) || [],
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -728,10 +801,7 @@ export class SystemForEmbassy implements System {
|
|||||||
? storedDependsOn
|
? storedDependsOn
|
||||||
: requiredDeps
|
: requiredDeps
|
||||||
|
|
||||||
await effects.store.set({
|
await dependsOnFile.write(effects, dependsOn)
|
||||||
path: EMBASSY_DEPENDS_ON_PATH_PREFIX,
|
|
||||||
value: dependsOn,
|
|
||||||
})
|
|
||||||
|
|
||||||
await effects.setDependencies({
|
await effects.setDependencies({
|
||||||
dependencies: Object.entries(dependsOn).flatMap(
|
dependencies: Object.entries(dependsOn).flatMap(
|
||||||
@@ -755,35 +825,37 @@ export class SystemForEmbassy implements System {
|
|||||||
|
|
||||||
async migration(
|
async migration(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
version: { from: string } | { to: string },
|
version:
|
||||||
|
| { from: VersionRange | ExtendedVersion }
|
||||||
|
| { to: VersionRange | ExtendedVersion },
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<{ configured: boolean }> {
|
): Promise<{ configured: boolean } | null> {
|
||||||
let migration
|
let migration
|
||||||
let args: [string, ...string[]]
|
let args: [string, ...string[]]
|
||||||
if ("from" in version) {
|
if ("from" in version) {
|
||||||
args = [version.from, "from"]
|
if (overlaps(this.version, version.from)) return null
|
||||||
const fromExver = ExtendedVersion.parse(version.from)
|
args = [version.from.toString(), "from"]
|
||||||
if (!this.manifest.migrations) return { configured: true }
|
if (!this.manifest.migrations) return { configured: true }
|
||||||
migration = Object.entries(this.manifest.migrations.from)
|
migration = Object.entries(this.manifest.migrations.from)
|
||||||
.map(
|
.map(
|
||||||
([version, procedure]) =>
|
([version, procedure]) =>
|
||||||
[VersionRange.parseEmver(version), procedure] as const,
|
[VersionRange.parseEmver(version), procedure] as const,
|
||||||
)
|
)
|
||||||
.find(([versionEmver, _]) => versionEmver.satisfiedBy(fromExver))
|
.find(([versionEmver, _]) => overlaps(versionEmver, version.from))
|
||||||
} else {
|
} else {
|
||||||
args = [version.to, "to"]
|
if (overlaps(this.version, version.to)) return null
|
||||||
const toExver = ExtendedVersion.parse(version.to)
|
args = [version.to.toString(), "to"]
|
||||||
if (!this.manifest.migrations) return { configured: true }
|
if (!this.manifest.migrations) return { configured: true }
|
||||||
migration = Object.entries(this.manifest.migrations.to)
|
migration = Object.entries(this.manifest.migrations.to)
|
||||||
.map(
|
.map(
|
||||||
([version, procedure]) =>
|
([version, procedure]) =>
|
||||||
[VersionRange.parseEmver(version), procedure] as const,
|
[VersionRange.parseEmver(version), procedure] as const,
|
||||||
)
|
)
|
||||||
.find(([versionEmver, _]) => versionEmver.satisfiedBy(toExver))
|
.find(([versionEmver, _]) => overlaps(versionEmver, version.to))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (migration) {
|
if (migration) {
|
||||||
const [_, procedure] = migration
|
const [_, procedure] = migration as readonly [unknown, Procedure]
|
||||||
if (procedure.type === "docker") {
|
if (procedure.type === "docker") {
|
||||||
const commands = [procedure.entrypoint, ...procedure.args]
|
const commands = [procedure.entrypoint, ...procedure.args]
|
||||||
const container = await DockerProcedureContainer.of(
|
const container = await DockerProcedureContainer.of(
|
||||||
@@ -815,14 +887,16 @@ export class SystemForEmbassy implements System {
|
|||||||
})) as any
|
})) as any
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { configured: true }
|
return null
|
||||||
}
|
}
|
||||||
async properties(
|
async properties(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<PropertiesReturn> {
|
): Promise<PropertiesReturn> {
|
||||||
// TODO BLU-J set the properties ever so often
|
const setConfigValue = this.manifest.properties as
|
||||||
const setConfigValue = this.manifest.properties
|
| Procedure
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
if (!setConfigValue) throw new Error("There is no properties")
|
if (!setConfigValue) throw new Error("There is no properties")
|
||||||
if (setConfigValue.type === "docker") {
|
if (setConfigValue.type === "docker") {
|
||||||
const commands = [setConfigValue.entrypoint, ...setConfigValue.args]
|
const commands = [setConfigValue.entrypoint, ...setConfigValue.args]
|
||||||
@@ -833,7 +907,7 @@ export class SystemForEmbassy implements System {
|
|||||||
this.manifest.volumes,
|
this.manifest.volumes,
|
||||||
`Properties - ${commands.join(" ")}`,
|
`Properties - ${commands.join(" ")}`,
|
||||||
)
|
)
|
||||||
const properties = matchProperties.unsafeCast(
|
const properties = matchProperties.parse(
|
||||||
JSON.parse(
|
JSON.parse(
|
||||||
(await container.execFail(commands, timeoutMs)).stdout.toString(),
|
(await container.execFail(commands, timeoutMs)).stdout.toString(),
|
||||||
),
|
),
|
||||||
@@ -844,7 +918,7 @@ export class SystemForEmbassy implements System {
|
|||||||
const method = moduleCode.properties
|
const method = moduleCode.properties
|
||||||
if (!method)
|
if (!method)
|
||||||
throw new Error("Expecting that the method properties exists")
|
throw new Error("Expecting that the method properties exists")
|
||||||
const properties = matchProperties.unsafeCast(
|
const properties = matchProperties.parse(
|
||||||
await method(polyfillEffects(effects, this.manifest)).then(
|
await method(polyfillEffects(effects, this.manifest)).then(
|
||||||
fromReturnType,
|
fromReturnType,
|
||||||
),
|
),
|
||||||
@@ -859,7 +933,8 @@ export class SystemForEmbassy implements System {
|
|||||||
formData: unknown,
|
formData: unknown,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<T.ActionResult> {
|
): Promise<T.ActionResult> {
|
||||||
const actionProcedure = this.manifest.actions?.[actionId]?.implementation
|
const actionProcedure = this.manifest.actions?.[actionId]
|
||||||
|
?.implementation as Procedure | undefined
|
||||||
const toActionResult = ({
|
const toActionResult = ({
|
||||||
message,
|
message,
|
||||||
value,
|
value,
|
||||||
@@ -926,7 +1001,9 @@ export class SystemForEmbassy implements System {
|
|||||||
oldConfig: unknown,
|
oldConfig: unknown,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<object> {
|
): Promise<object> {
|
||||||
const actionProcedure = this.manifest.dependencies?.[id]?.config?.check
|
const actionProcedure = this.manifest.dependencies?.[id]?.config?.check as
|
||||||
|
| Procedure
|
||||||
|
| undefined
|
||||||
if (!actionProcedure) return { message: "Action not found", value: null }
|
if (!actionProcedure) return { message: "Action not found", value: null }
|
||||||
if (actionProcedure.type === "docker") {
|
if (actionProcedure.type === "docker") {
|
||||||
const commands = [
|
const commands = [
|
||||||
@@ -969,80 +1046,99 @@ export class SystemForEmbassy implements System {
|
|||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// TODO: docker
|
// TODO: docker
|
||||||
const oldConfig = (await effects.store.get({
|
await effects.mount({
|
||||||
packageId: id,
|
location: `/media/embassy/${id}`,
|
||||||
path: EMBASSY_POINTER_PATH_PREFIX,
|
target: {
|
||||||
callback: () => {
|
|
||||||
this.dependenciesAutoconfig(effects, id, timeoutMs)
|
|
||||||
},
|
|
||||||
})) as U.Config
|
|
||||||
if (!oldConfig) return
|
|
||||||
const moduleCode = await this.moduleCode
|
|
||||||
const method = moduleCode?.dependencies?.[id]?.autoConfigure
|
|
||||||
if (!method) return
|
|
||||||
const newConfig = (await method(
|
|
||||||
polyfillEffects(effects, this.manifest),
|
|
||||||
JSON.parse(JSON.stringify(oldConfig)),
|
|
||||||
).then((x) => {
|
|
||||||
if ("result" in x) return x.result
|
|
||||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
|
||||||
throw new Error("Error getting config: " + x["error-code"][1])
|
|
||||||
})) as any
|
|
||||||
const diff = partialDiff(oldConfig, newConfig)
|
|
||||||
if (diff) {
|
|
||||||
await effects.action.request({
|
|
||||||
actionId: "config",
|
|
||||||
packageId: id,
|
packageId: id,
|
||||||
replayId: `${id}/config`,
|
volumeId: "embassy",
|
||||||
severity: "important",
|
subpath: null,
|
||||||
reason: `Configure this dependency for the needs of ${this.manifest.title}`,
|
readonly: true,
|
||||||
input: {
|
idmap: [],
|
||||||
kind: "partial",
|
},
|
||||||
value: diff.diff,
|
})
|
||||||
},
|
configFile
|
||||||
when: {
|
.withPath(`/media/embassy/${id}/config.json`)
|
||||||
condition: "input-not-matches",
|
.read()
|
||||||
once: false,
|
.onChange(effects, async (oldConfig: U.Config) => {
|
||||||
},
|
if (!oldConfig) return { cancel: false }
|
||||||
|
const moduleCode = await this.moduleCode
|
||||||
|
const method = moduleCode?.dependencies?.[id]?.autoConfigure
|
||||||
|
if (!method) return { cancel: true }
|
||||||
|
const newConfig = (await method(
|
||||||
|
polyfillEffects(effects, this.manifest),
|
||||||
|
JSON.parse(JSON.stringify(oldConfig)),
|
||||||
|
).then((x) => {
|
||||||
|
if ("result" in x) return x.result
|
||||||
|
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||||
|
throw new Error("Error getting config: " + x["error-code"][1])
|
||||||
|
})) as any
|
||||||
|
const diff = partialDiff(oldConfig, newConfig)
|
||||||
|
if (diff) {
|
||||||
|
await effects.action.createTask({
|
||||||
|
actionId: "config",
|
||||||
|
packageId: id,
|
||||||
|
replayId: `${id}/config`,
|
||||||
|
severity: "important",
|
||||||
|
reason: `Configure this dependency for the needs of ${this.manifest.title}`,
|
||||||
|
input: {
|
||||||
|
kind: "partial",
|
||||||
|
value: diff.diff,
|
||||||
|
},
|
||||||
|
when: {
|
||||||
|
condition: "input-not-matches",
|
||||||
|
once: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { cancel: false }
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchPointer = object({
|
const matchPointer = z.object({
|
||||||
type: literal("pointer"),
|
type: z.literal("pointer"),
|
||||||
})
|
})
|
||||||
|
|
||||||
const matchPointerPackage = object({
|
const matchPointerPackage = z.object({
|
||||||
subtype: literal("package"),
|
subtype: z.literal("package"),
|
||||||
target: literals("tor-key", "tor-address", "lan-address"),
|
target: z.enum(["tor-key", "tor-address", "lan-address"]),
|
||||||
"package-id": string,
|
"package-id": z.string(),
|
||||||
interface: string,
|
interface: z.string(),
|
||||||
})
|
})
|
||||||
const matchPointerConfig = object({
|
const matchPointerConfig = z.object({
|
||||||
subtype: literal("package"),
|
subtype: z.literal("package"),
|
||||||
target: literals("config"),
|
target: z.enum(["config"]),
|
||||||
"package-id": string,
|
"package-id": z.string(),
|
||||||
selector: string,
|
selector: z.string(),
|
||||||
multi: boolean,
|
multi: z.boolean(),
|
||||||
})
|
})
|
||||||
const matchSpec = object({
|
const matchSpec = z.object({
|
||||||
spec: object,
|
spec: z.record(z.string(), z.unknown()),
|
||||||
})
|
})
|
||||||
const matchVariants = object({ variants: dictionary([string, unknown]) })
|
const matchVariants = z.object({ variants: z.record(z.string(), z.unknown()) })
|
||||||
|
function isMatchPointer(v: unknown): v is z.infer<typeof matchPointer> {
|
||||||
|
return matchPointer.safeParse(v).success
|
||||||
|
}
|
||||||
|
function isMatchSpec(v: unknown): v is z.infer<typeof matchSpec> {
|
||||||
|
return matchSpec.safeParse(v).success
|
||||||
|
}
|
||||||
|
function isMatchVariants(v: unknown): v is z.infer<typeof matchVariants> {
|
||||||
|
return matchVariants.safeParse(v).success
|
||||||
|
}
|
||||||
function cleanSpecOfPointers<T>(mutSpec: T): T {
|
function cleanSpecOfPointers<T>(mutSpec: T): T {
|
||||||
if (!object.test(mutSpec)) return mutSpec
|
if (typeof mutSpec !== "object" || mutSpec === null) return mutSpec
|
||||||
for (const key in mutSpec) {
|
for (const key in mutSpec) {
|
||||||
const value = mutSpec[key]
|
const value = mutSpec[key]
|
||||||
if (matchSpec.test(value)) value.spec = cleanSpecOfPointers(value.spec)
|
if (isMatchSpec(value))
|
||||||
if (matchVariants.test(value))
|
value.spec = cleanSpecOfPointers(value.spec) as Record<string, unknown>
|
||||||
|
if (isMatchVariants(value))
|
||||||
value.variants = Object.fromEntries(
|
value.variants = Object.fromEntries(
|
||||||
Object.entries(value.variants).map(([key, value]) => [
|
Object.entries(value.variants).map(([key, value]) => [
|
||||||
key,
|
key,
|
||||||
cleanSpecOfPointers(value),
|
cleanSpecOfPointers(value),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
if (!matchPointer.test(value)) continue
|
if (!isMatchPointer(value)) continue
|
||||||
delete mutSpec[key]
|
delete mutSpec[key]
|
||||||
// // if (value.target === )
|
// // if (value.target === )
|
||||||
}
|
}
|
||||||
@@ -1107,11 +1203,21 @@ async function updateConfig(
|
|||||||
) {
|
) {
|
||||||
if (specValue.target === "config") {
|
if (specValue.target === "config") {
|
||||||
const jp = require("jsonpath")
|
const jp = require("jsonpath")
|
||||||
const remoteConfig = await effects.store.get({
|
const depId = specValue["package-id"]
|
||||||
packageId: specValue["package-id"],
|
await effects.mount({
|
||||||
callback: () => effects.restart(),
|
location: `/media/embassy/${depId}`,
|
||||||
path: EMBASSY_POINTER_PATH_PREFIX,
|
target: {
|
||||||
|
packageId: depId,
|
||||||
|
volumeId: "embassy",
|
||||||
|
subpath: null,
|
||||||
|
readonly: true,
|
||||||
|
idmap: [],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
const remoteConfig = configFile
|
||||||
|
.withPath(`/media/embassy/${depId}/config.json`)
|
||||||
|
.read()
|
||||||
|
.once()
|
||||||
console.debug(remoteConfig)
|
console.debug(remoteConfig)
|
||||||
const configValue = specValue.multi
|
const configValue = specValue.multi
|
||||||
? jp.query(remoteConfig, specValue.selector)
|
? jp.query(remoteConfig, specValue.selector)
|
||||||
@@ -1152,14 +1258,10 @@ async function updateConfig(
|
|||||||
const url: string =
|
const url: string =
|
||||||
filled === null || filled.addressInfo === null
|
filled === null || filled.addressInfo === null
|
||||||
? ""
|
? ""
|
||||||
: catchFn(() =>
|
: catchFn(
|
||||||
utils.hostnameInfoToAddress(
|
() =>
|
||||||
specValue.target === "lan-address"
|
filled.addressInfo!.filter({ kind: "mdns" })!.hostnames[0]
|
||||||
? filled.addressInfo!.localHostnames[0] ||
|
.hostname,
|
||||||
filled.addressInfo!.onionHostnames[0]
|
|
||||||
: filled.addressInfo!.onionHostnames[0] ||
|
|
||||||
filled.addressInfo!.localHostnames[0],
|
|
||||||
),
|
|
||||||
) || ""
|
) || ""
|
||||||
mutConfigValue[key] = url
|
mutConfigValue[key] = url
|
||||||
}
|
}
|
||||||
@@ -1182,7 +1284,7 @@ function extractServiceInterfaceId(manifest: Manifest, specInterface: string) {
|
|||||||
}
|
}
|
||||||
async function convertToNewConfig(value: OldGetConfigRes) {
|
async function convertToNewConfig(value: OldGetConfigRes) {
|
||||||
try {
|
try {
|
||||||
const valueSpec: OldConfigSpec = matchOldConfigSpec.unsafeCast(value.spec)
|
const valueSpec: OldConfigSpec = matchOldConfigSpec.parse(value.spec)
|
||||||
const spec = transformConfigSpec(valueSpec)
|
const spec = transformConfigSpec(valueSpec)
|
||||||
if (!value.config) return { spec, config: null }
|
if (!value.config) return { spec, config: null }
|
||||||
const config = transformOldConfigToNew(valueSpec, value.config) ?? null
|
const config = transformOldConfigToNew(valueSpec, value.config) ?? null
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import synapseManifest from "./__fixtures__/synapseManifest"
|
|||||||
|
|
||||||
describe("matchManifest", () => {
|
describe("matchManifest", () => {
|
||||||
test("gittea", () => {
|
test("gittea", () => {
|
||||||
matchManifest.unsafeCast(giteaManifest)
|
matchManifest.parse(giteaManifest)
|
||||||
})
|
})
|
||||||
test("synapse", () => {
|
test("synapse", () => {
|
||||||
matchManifest.unsafeCast(synapseManifest)
|
matchManifest.parse(synapseManifest)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,136 +1,121 @@
|
|||||||
import {
|
import { z } from "@start9labs/start-sdk"
|
||||||
object,
|
|
||||||
literal,
|
|
||||||
string,
|
|
||||||
array,
|
|
||||||
boolean,
|
|
||||||
dictionary,
|
|
||||||
literals,
|
|
||||||
number,
|
|
||||||
unknown,
|
|
||||||
some,
|
|
||||||
every,
|
|
||||||
} from "ts-matches"
|
|
||||||
import { matchVolume } from "./matchVolume"
|
import { matchVolume } from "./matchVolume"
|
||||||
import { matchDockerProcedure } from "../../../Models/DockerProcedure"
|
import { matchDockerProcedure } from "../../../Models/DockerProcedure"
|
||||||
|
|
||||||
const matchJsProcedure = object(
|
const matchJsProcedure = z.object({
|
||||||
{
|
type: z.literal("script"),
|
||||||
type: literal("script"),
|
args: z.array(z.unknown()).nullable().optional().default([]),
|
||||||
args: array(unknown),
|
})
|
||||||
},
|
|
||||||
["args"],
|
|
||||||
{
|
|
||||||
args: [],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const matchProcedure = some(matchDockerProcedure, matchJsProcedure)
|
const matchProcedure = z.union([matchDockerProcedure, matchJsProcedure])
|
||||||
export type Procedure = typeof matchProcedure._TYPE
|
export type Procedure = z.infer<typeof matchProcedure>
|
||||||
|
|
||||||
const matchAction = object(
|
const matchAction = z.object({
|
||||||
{
|
name: z.string(),
|
||||||
name: string,
|
description: z.string(),
|
||||||
description: string,
|
warning: z.string().nullable().optional(),
|
||||||
warning: string,
|
implementation: matchProcedure,
|
||||||
implementation: matchProcedure,
|
"allowed-statuses": z.array(z.enum(["running", "stopped"])),
|
||||||
"allowed-statuses": array(literals("running", "stopped")),
|
"input-spec": z.unknown().nullable().optional(),
|
||||||
"input-spec": unknown,
|
})
|
||||||
},
|
export const matchManifest = z.object({
|
||||||
["warning", "input-spec", "input-spec"],
|
id: z.string(),
|
||||||
)
|
title: z.string(),
|
||||||
export const matchManifest = object(
|
version: z.string(),
|
||||||
{
|
main: matchDockerProcedure,
|
||||||
id: string,
|
assets: z
|
||||||
title: string,
|
.object({
|
||||||
version: string,
|
assets: z.string().nullable().optional(),
|
||||||
main: matchDockerProcedure,
|
scripts: z.string().nullable().optional(),
|
||||||
assets: object(
|
})
|
||||||
{
|
.nullable()
|
||||||
assets: string,
|
.optional(),
|
||||||
scripts: string,
|
"health-checks": z.record(
|
||||||
},
|
z.string(),
|
||||||
["assets", "scripts"],
|
z.intersection(
|
||||||
|
matchProcedure,
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
"success-message": z.string().nullable().optional(),
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
"health-checks": dictionary([
|
),
|
||||||
string,
|
config: z
|
||||||
every(
|
.object({
|
||||||
matchProcedure,
|
|
||||||
object(
|
|
||||||
{
|
|
||||||
name: string,
|
|
||||||
["success-message"]: string,
|
|
||||||
},
|
|
||||||
["success-message"],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
config: object({
|
|
||||||
get: matchProcedure,
|
get: matchProcedure,
|
||||||
set: matchProcedure,
|
set: matchProcedure,
|
||||||
}),
|
})
|
||||||
properties: matchProcedure,
|
.nullable()
|
||||||
volumes: dictionary([string, matchVolume]),
|
.optional(),
|
||||||
interfaces: dictionary([
|
properties: matchProcedure.nullable().optional(),
|
||||||
string,
|
volumes: z.record(z.string(), matchVolume),
|
||||||
object(
|
interfaces: z.record(
|
||||||
{
|
z.string(),
|
||||||
name: string,
|
z.object({
|
||||||
description: string,
|
name: z.string(),
|
||||||
"tor-config": object({
|
description: z.string(),
|
||||||
"port-mapping": dictionary([string, string]),
|
"tor-config": z
|
||||||
|
.object({
|
||||||
|
"port-mapping": z.record(z.string(), z.string()),
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
"lan-config": z
|
||||||
|
.record(
|
||||||
|
z.string(),
|
||||||
|
z.object({
|
||||||
|
ssl: z.boolean(),
|
||||||
|
internal: z.number(),
|
||||||
}),
|
}),
|
||||||
"lan-config": dictionary([
|
)
|
||||||
string,
|
.nullable()
|
||||||
object({
|
.optional(),
|
||||||
ssl: boolean,
|
ui: z.boolean(),
|
||||||
internal: number,
|
protocols: z.array(z.string()),
|
||||||
}),
|
|
||||||
]),
|
|
||||||
ui: boolean,
|
|
||||||
protocols: array(string),
|
|
||||||
},
|
|
||||||
["lan-config", "tor-config"],
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
backup: object({
|
|
||||||
create: matchProcedure,
|
|
||||||
restore: matchProcedure,
|
|
||||||
}),
|
}),
|
||||||
migrations: object({
|
),
|
||||||
to: dictionary([string, matchProcedure]),
|
backup: z.object({
|
||||||
from: dictionary([string, matchProcedure]),
|
create: matchProcedure,
|
||||||
}),
|
restore: matchProcedure,
|
||||||
dependencies: dictionary([
|
}),
|
||||||
string,
|
migrations: z
|
||||||
object(
|
.object({
|
||||||
{
|
to: z.record(z.string(), matchProcedure),
|
||||||
version: string,
|
from: z.record(z.string(), matchProcedure),
|
||||||
requirement: some(
|
})
|
||||||
object({
|
.nullable()
|
||||||
type: literal("opt-in"),
|
.optional(),
|
||||||
how: string,
|
dependencies: z.record(
|
||||||
}),
|
z.string(),
|
||||||
object({
|
z
|
||||||
type: literal("opt-out"),
|
.object({
|
||||||
how: string,
|
version: z.string(),
|
||||||
}),
|
requirement: z.union([
|
||||||
object({
|
z.object({
|
||||||
type: literal("required"),
|
type: z.literal("opt-in"),
|
||||||
}),
|
how: z.string(),
|
||||||
),
|
}),
|
||||||
description: string,
|
z.object({
|
||||||
config: object({
|
type: z.literal("opt-out"),
|
||||||
|
how: z.string(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal("required"),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
description: z.string().nullable().optional(),
|
||||||
|
config: z
|
||||||
|
.object({
|
||||||
check: matchProcedure,
|
check: matchProcedure,
|
||||||
"auto-configure": matchProcedure,
|
"auto-configure": matchProcedure,
|
||||||
}),
|
})
|
||||||
},
|
.nullable()
|
||||||
["description", "config"],
|
.optional(),
|
||||||
),
|
})
|
||||||
]),
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
),
|
||||||
|
|
||||||
actions: dictionary([string, matchAction]),
|
actions: z.record(z.string(), matchAction),
|
||||||
},
|
})
|
||||||
["config", "actions", "properties", "migrations", "dependencies"],
|
export type Manifest = z.infer<typeof matchManifest>
|
||||||
)
|
|
||||||
export type Manifest = typeof matchManifest._TYPE
|
|
||||||
|
|||||||
@@ -1,35 +1,32 @@
|
|||||||
import { object, literal, string, boolean, some } from "ts-matches"
|
import { z } from "@start9labs/start-sdk"
|
||||||
|
|
||||||
const matchDataVolume = object(
|
const matchDataVolume = z.object({
|
||||||
{
|
type: z.literal("data"),
|
||||||
type: literal("data"),
|
readonly: z.boolean().optional(),
|
||||||
readonly: boolean,
|
|
||||||
},
|
|
||||||
["readonly"],
|
|
||||||
)
|
|
||||||
const matchAssetVolume = object({
|
|
||||||
type: literal("assets"),
|
|
||||||
})
|
})
|
||||||
const matchPointerVolume = object({
|
const matchAssetVolume = z.object({
|
||||||
type: literal("pointer"),
|
type: z.literal("assets"),
|
||||||
"package-id": string,
|
|
||||||
"volume-id": string,
|
|
||||||
path: string,
|
|
||||||
readonly: boolean,
|
|
||||||
})
|
})
|
||||||
const matchCertificateVolume = object({
|
const matchPointerVolume = z.object({
|
||||||
type: literal("certificate"),
|
type: z.literal("pointer"),
|
||||||
"interface-id": string,
|
"package-id": z.string(),
|
||||||
|
"volume-id": z.string(),
|
||||||
|
path: z.string(),
|
||||||
|
readonly: z.boolean(),
|
||||||
})
|
})
|
||||||
const matchBackupVolume = object({
|
const matchCertificateVolume = z.object({
|
||||||
type: literal("backup"),
|
type: z.literal("certificate"),
|
||||||
readonly: boolean,
|
"interface-id": z.string(),
|
||||||
})
|
})
|
||||||
export const matchVolume = some(
|
const matchBackupVolume = z.object({
|
||||||
|
type: z.literal("backup"),
|
||||||
|
readonly: z.boolean(),
|
||||||
|
})
|
||||||
|
export const matchVolume = z.union([
|
||||||
matchDataVolume,
|
matchDataVolume,
|
||||||
matchAssetVolume,
|
matchAssetVolume,
|
||||||
matchPointerVolume,
|
matchPointerVolume,
|
||||||
matchCertificateVolume,
|
matchCertificateVolume,
|
||||||
matchBackupVolume,
|
matchBackupVolume,
|
||||||
)
|
])
|
||||||
export type Volume = typeof matchVolume._TYPE
|
export type Volume = z.infer<typeof matchVolume>
|
||||||
|
|||||||
@@ -135,12 +135,9 @@ export const polyfillEffects = (
|
|||||||
[input.command, ...(input.args || [])].join(" "),
|
[input.command, ...(input.args || [])].join(" "),
|
||||||
)
|
)
|
||||||
const daemon = promiseSubcontainer.then((subcontainer) =>
|
const daemon = promiseSubcontainer.then((subcontainer) =>
|
||||||
daemons.runCommand()(
|
daemons.runCommand()(effects, subcontainer, {
|
||||||
effects,
|
command: [input.command, ...(input.args || [])],
|
||||||
subcontainer,
|
}),
|
||||||
[input.command, ...(input.args || [])],
|
|
||||||
{},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
wait: () =>
|
wait: () =>
|
||||||
@@ -169,12 +166,12 @@ export const polyfillEffects = (
|
|||||||
{ imageId: manifest.main.image },
|
{ imageId: manifest.main.image },
|
||||||
commands,
|
commands,
|
||||||
{
|
{
|
||||||
mounts: Mounts.of().addVolume(
|
mounts: Mounts.of().mountVolume({
|
||||||
input.volumeId,
|
volumeId: input.volumeId,
|
||||||
null,
|
subpath: null,
|
||||||
"/drive",
|
mountpoint: "/drive",
|
||||||
false,
|
readonly: false,
|
||||||
),
|
}),
|
||||||
},
|
},
|
||||||
commands.join(" "),
|
commands.join(" "),
|
||||||
)
|
)
|
||||||
@@ -206,12 +203,12 @@ export const polyfillEffects = (
|
|||||||
{ imageId: manifest.main.image },
|
{ imageId: manifest.main.image },
|
||||||
commands,
|
commands,
|
||||||
{
|
{
|
||||||
mounts: Mounts.of().addVolume(
|
mounts: Mounts.of().mountVolume({
|
||||||
input.volumeId,
|
volumeId: input.volumeId,
|
||||||
null,
|
subpath: null,
|
||||||
"/drive",
|
mountpoint: "/drive",
|
||||||
false,
|
readonly: false,
|
||||||
),
|
}),
|
||||||
},
|
},
|
||||||
commands.join(" "),
|
commands.join(" "),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { matchOldConfigSpec, transformConfigSpec } from "./transformConfigSpec"
|
import {
|
||||||
import fixtureEmbasyPagesConfig from "./__fixtures__/embasyPagesConfig"
|
matchOldConfigSpec,
|
||||||
|
matchOldValueSpecList,
|
||||||
|
transformConfigSpec,
|
||||||
|
} from "./transformConfigSpec"
|
||||||
|
import fixtureEmbassyPagesConfig from "./__fixtures__/embassyPagesConfig"
|
||||||
|
import fixtureRTLConfig from "./__fixtures__/rtlConfig"
|
||||||
import searNXG from "./__fixtures__/searNXG"
|
import searNXG from "./__fixtures__/searNXG"
|
||||||
import bitcoind from "./__fixtures__/bitcoind"
|
import bitcoind from "./__fixtures__/bitcoind"
|
||||||
import nostr from "./__fixtures__/nostr"
|
import nostr from "./__fixtures__/nostr"
|
||||||
@@ -7,32 +12,43 @@ import nostrConfig2 from "./__fixtures__/nostrConfig2"
|
|||||||
|
|
||||||
describe("transformConfigSpec", () => {
|
describe("transformConfigSpec", () => {
|
||||||
test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => {
|
test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => {
|
||||||
matchOldConfigSpec.unsafeCast(
|
matchOldConfigSpec.parse(
|
||||||
fixtureEmbasyPagesConfig.homepage.variants["web-page"],
|
fixtureEmbassyPagesConfig.homepage.variants["web-page"],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
test("matchOldConfigSpec(embassyPages)", () => {
|
test("matchOldConfigSpec(embassyPages)", () => {
|
||||||
matchOldConfigSpec.unsafeCast(fixtureEmbasyPagesConfig)
|
matchOldConfigSpec.parse(fixtureEmbassyPagesConfig)
|
||||||
})
|
})
|
||||||
test("transformConfigSpec(embassyPages)", () => {
|
test("transformConfigSpec(embassyPages)", () => {
|
||||||
const spec = matchOldConfigSpec.unsafeCast(fixtureEmbasyPagesConfig)
|
const spec = matchOldConfigSpec.parse(fixtureEmbassyPagesConfig)
|
||||||
|
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("matchOldConfigSpec(RTL.nodes)", () => {
|
||||||
|
matchOldValueSpecList.parse(fixtureRTLConfig.nodes)
|
||||||
|
})
|
||||||
|
test("matchOldConfigSpec(RTL)", () => {
|
||||||
|
matchOldConfigSpec.parse(fixtureRTLConfig)
|
||||||
|
})
|
||||||
|
test("transformConfigSpec(RTL)", () => {
|
||||||
|
const spec = matchOldConfigSpec.parse(fixtureRTLConfig)
|
||||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("transformConfigSpec(searNXG)", () => {
|
test("transformConfigSpec(searNXG)", () => {
|
||||||
const spec = matchOldConfigSpec.unsafeCast(searNXG)
|
const spec = matchOldConfigSpec.parse(searNXG)
|
||||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
test("transformConfigSpec(bitcoind)", () => {
|
test("transformConfigSpec(bitcoind)", () => {
|
||||||
const spec = matchOldConfigSpec.unsafeCast(bitcoind)
|
const spec = matchOldConfigSpec.parse(bitcoind)
|
||||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
test("transformConfigSpec(nostr)", () => {
|
test("transformConfigSpec(nostr)", () => {
|
||||||
const spec = matchOldConfigSpec.unsafeCast(nostr)
|
const spec = matchOldConfigSpec.parse(nostr)
|
||||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
test("transformConfigSpec(nostr2)", () => {
|
test("transformConfigSpec(nostr2)", () => {
|
||||||
const spec = matchOldConfigSpec.unsafeCast(nostrConfig2)
|
const spec = matchOldConfigSpec.parse(nostrConfig2)
|
||||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,19 +1,4 @@
|
|||||||
import { IST } from "@start9labs/start-sdk"
|
import { IST, z } from "@start9labs/start-sdk"
|
||||||
import {
|
|
||||||
dictionary,
|
|
||||||
object,
|
|
||||||
anyOf,
|
|
||||||
string,
|
|
||||||
literals,
|
|
||||||
array,
|
|
||||||
number,
|
|
||||||
boolean,
|
|
||||||
Parser,
|
|
||||||
deferred,
|
|
||||||
every,
|
|
||||||
nill,
|
|
||||||
literal,
|
|
||||||
} from "ts-matches"
|
|
||||||
|
|
||||||
export function transformConfigSpec(oldSpec: OldConfigSpec): IST.InputSpec {
|
export function transformConfigSpec(oldSpec: OldConfigSpec): IST.InputSpec {
|
||||||
return Object.entries(oldSpec).reduce((inputSpec, [key, oldVal]) => {
|
return Object.entries(oldSpec).reduce((inputSpec, [key, oldVal]) => {
|
||||||
@@ -47,6 +32,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): IST.InputSpec {
|
|||||||
immutable: false,
|
immutable: false,
|
||||||
}
|
}
|
||||||
} else if (oldVal.type === "list") {
|
} else if (oldVal.type === "list") {
|
||||||
|
if (isUnionList(oldVal)) return inputSpec
|
||||||
newVal = getListSpec(oldVal)
|
newVal = getListSpec(oldVal)
|
||||||
} else if (oldVal.type === "number") {
|
} else if (oldVal.type === "number") {
|
||||||
const range = Range.from(oldVal.range)
|
const range = Range.from(oldVal.range)
|
||||||
@@ -81,7 +67,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): IST.InputSpec {
|
|||||||
name: oldVal.name,
|
name: oldVal.name,
|
||||||
description: oldVal.description || null,
|
description: oldVal.description || null,
|
||||||
warning: oldVal.warning || null,
|
warning: oldVal.warning || null,
|
||||||
spec: transformConfigSpec(matchOldConfigSpec.unsafeCast(oldVal.spec)),
|
spec: transformConfigSpec(matchOldConfigSpec.parse(oldVal.spec)),
|
||||||
}
|
}
|
||||||
} else if (oldVal.type === "string") {
|
} else if (oldVal.type === "string") {
|
||||||
newVal = {
|
newVal = {
|
||||||
@@ -120,7 +106,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): IST.InputSpec {
|
|||||||
...obj,
|
...obj,
|
||||||
[id]: {
|
[id]: {
|
||||||
name: oldVal.tag["variant-names"][id] || id,
|
name: oldVal.tag["variant-names"][id] || id,
|
||||||
spec: transformConfigSpec(matchOldConfigSpec.unsafeCast(spec)),
|
spec: transformConfigSpec(matchOldConfigSpec.parse(spec)),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{} as Record<string, { name: string; spec: IST.InputSpec }>,
|
{} as Record<string, { name: string; spec: IST.InputSpec }>,
|
||||||
@@ -152,7 +138,7 @@ export function transformOldConfigToNew(
|
|||||||
|
|
||||||
if (isObject(val)) {
|
if (isObject(val)) {
|
||||||
newVal = transformOldConfigToNew(
|
newVal = transformOldConfigToNew(
|
||||||
matchOldConfigSpec.unsafeCast(val.spec),
|
matchOldConfigSpec.parse(val.spec),
|
||||||
config[key],
|
config[key],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -171,21 +157,20 @@ export function transformOldConfigToNew(
|
|||||||
newVal = {
|
newVal = {
|
||||||
selection,
|
selection,
|
||||||
value: transformOldConfigToNew(
|
value: transformOldConfigToNew(
|
||||||
matchOldConfigSpec.unsafeCast(val.variants[selection]),
|
matchOldConfigSpec.parse(val.variants[selection]),
|
||||||
config[key],
|
config[key],
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isList(val) && isObjectList(val)) {
|
if (isList(val)) {
|
||||||
if (!config[key]) return obj
|
if (!config[key]) return obj
|
||||||
|
|
||||||
newVal = (config[key] as object[]).map((obj) =>
|
if (isObjectList(val)) {
|
||||||
transformOldConfigToNew(
|
newVal = (config[key] as object[]).map((obj) =>
|
||||||
matchOldConfigSpec.unsafeCast(val.spec.spec),
|
transformOldConfigToNew(matchOldConfigSpec.parse(val.spec.spec), obj),
|
||||||
obj,
|
)
|
||||||
),
|
} else if (isUnionList(val)) return obj
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPointer(val)) {
|
if (isPointer(val)) {
|
||||||
@@ -203,12 +188,13 @@ export function transformNewConfigToOld(
|
|||||||
spec: OldConfigSpec,
|
spec: OldConfigSpec,
|
||||||
config: Record<string, any>,
|
config: Record<string, any>,
|
||||||
): Record<string, any> {
|
): Record<string, any> {
|
||||||
|
if (!config) return config
|
||||||
return Object.entries(spec).reduce((obj, [key, val]) => {
|
return Object.entries(spec).reduce((obj, [key, val]) => {
|
||||||
let newVal = config[key]
|
let newVal = config[key]
|
||||||
|
|
||||||
if (isObject(val)) {
|
if (isObject(val)) {
|
||||||
newVal = transformNewConfigToOld(
|
newVal = transformNewConfigToOld(
|
||||||
matchOldConfigSpec.unsafeCast(val.spec),
|
matchOldConfigSpec.parse(val.spec),
|
||||||
config[key],
|
config[key],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -217,19 +203,18 @@ export function transformNewConfigToOld(
|
|||||||
newVal = {
|
newVal = {
|
||||||
[val.tag.id]: config[key].selection,
|
[val.tag.id]: config[key].selection,
|
||||||
...transformNewConfigToOld(
|
...transformNewConfigToOld(
|
||||||
matchOldConfigSpec.unsafeCast(val.variants[config[key].selection]),
|
matchOldConfigSpec.parse(val.variants[config[key].selection]),
|
||||||
config[key].value,
|
config[key].value,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isList(val) && isObjectList(val)) {
|
if (isList(val)) {
|
||||||
newVal = (config[key] as object[]).map((obj) =>
|
if (isObjectList(val)) {
|
||||||
transformNewConfigToOld(
|
newVal = (config[key] as object[]).map((obj) =>
|
||||||
matchOldConfigSpec.unsafeCast(val.spec.spec),
|
transformNewConfigToOld(matchOldConfigSpec.parse(val.spec.spec), obj),
|
||||||
obj,
|
)
|
||||||
),
|
} else if (isUnionList(val)) return obj
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -331,9 +316,7 @@ function getListSpec(
|
|||||||
default: oldVal.default as Record<string, unknown>[],
|
default: oldVal.default as Record<string, unknown>[],
|
||||||
spec: {
|
spec: {
|
||||||
type: "object",
|
type: "object",
|
||||||
spec: transformConfigSpec(
|
spec: transformConfigSpec(matchOldConfigSpec.parse(oldVal.spec.spec)),
|
||||||
matchOldConfigSpec.unsafeCast(oldVal.spec.spec),
|
|
||||||
),
|
|
||||||
uniqueBy: oldVal.spec["unique-by"] || null,
|
uniqueBy: oldVal.spec["unique-by"] || null,
|
||||||
displayAs: oldVal.spec["display-as"] || null,
|
displayAs: oldVal.spec["display-as"] || null,
|
||||||
},
|
},
|
||||||
@@ -375,252 +358,293 @@ function isNumberList(
|
|||||||
): val is OldValueSpecList & { subtype: "number" } {
|
): val is OldValueSpecList & { subtype: "number" } {
|
||||||
return val.subtype === "number"
|
return val.subtype === "number"
|
||||||
}
|
}
|
||||||
|
|
||||||
function isObjectList(
|
function isObjectList(
|
||||||
val: OldValueSpecList,
|
val: OldValueSpecList,
|
||||||
): val is OldValueSpecList & { subtype: "object" } {
|
): val is OldValueSpecList & { subtype: "object" } {
|
||||||
if (["union"].includes(val.subtype)) {
|
|
||||||
throw new Error("Invalid list subtype. enum, string, and object permitted.")
|
|
||||||
}
|
|
||||||
return val.subtype === "object"
|
return val.subtype === "object"
|
||||||
}
|
}
|
||||||
|
function isUnionList(
|
||||||
|
val: OldValueSpecList,
|
||||||
|
): val is OldValueSpecList & { subtype: "union" } {
|
||||||
|
return val.subtype === "union"
|
||||||
|
}
|
||||||
|
|
||||||
export type OldConfigSpec = Record<string, OldValueSpec>
|
export type OldConfigSpec = Record<string, OldValueSpec>
|
||||||
const [_matchOldConfigSpec, setMatchOldConfigSpec] = deferred<unknown>()
|
export const matchOldConfigSpec: z.ZodType<OldConfigSpec> = z.lazy(() =>
|
||||||
export const matchOldConfigSpec = _matchOldConfigSpec as Parser<
|
z.record(z.string(), matchOldValueSpec),
|
||||||
unknown,
|
|
||||||
OldConfigSpec
|
|
||||||
>
|
|
||||||
export const matchOldDefaultString = anyOf(
|
|
||||||
string,
|
|
||||||
object({ charset: string, len: number }),
|
|
||||||
)
|
)
|
||||||
type OldDefaultString = typeof matchOldDefaultString._TYPE
|
export const matchOldDefaultString = z.union([
|
||||||
|
z.string(),
|
||||||
|
z.object({ charset: z.string(), len: z.number() }),
|
||||||
|
])
|
||||||
|
type OldDefaultString = z.infer<typeof matchOldDefaultString>
|
||||||
|
|
||||||
export const matchOldValueSpecString = object(
|
export const matchOldValueSpecString = z.object({
|
||||||
{
|
type: z.enum(["string"]),
|
||||||
type: literals("string"),
|
name: z.string(),
|
||||||
name: string,
|
masked: z.boolean().nullable().optional(),
|
||||||
masked: boolean,
|
copyable: z.boolean().nullable().optional(),
|
||||||
copyable: boolean,
|
nullable: z.boolean().nullable().optional(),
|
||||||
nullable: boolean,
|
placeholder: z.string().nullable().optional(),
|
||||||
placeholder: string,
|
pattern: z.string().nullable().optional(),
|
||||||
pattern: string,
|
"pattern-description": z.string().nullable().optional(),
|
||||||
"pattern-description": string,
|
default: matchOldDefaultString.nullable().optional(),
|
||||||
default: matchOldDefaultString,
|
textarea: z.boolean().nullable().optional(),
|
||||||
textarea: boolean,
|
description: z.string().nullable().optional(),
|
||||||
description: string,
|
warning: z.string().nullable().optional(),
|
||||||
warning: string,
|
})
|
||||||
},
|
|
||||||
[
|
export const matchOldValueSpecNumber = z.object({
|
||||||
"masked",
|
type: z.enum(["number"]),
|
||||||
"copyable",
|
nullable: z.boolean(),
|
||||||
"nullable",
|
name: z.string(),
|
||||||
"placeholder",
|
range: z.string(),
|
||||||
"pattern",
|
integral: z.boolean(),
|
||||||
"pattern-description",
|
default: z.number().nullable().optional(),
|
||||||
"default",
|
description: z.string().nullable().optional(),
|
||||||
"textarea",
|
warning: z.string().nullable().optional(),
|
||||||
"description",
|
units: z.string().nullable().optional(),
|
||||||
"warning",
|
placeholder: z.union([z.number(), z.string()]).nullable().optional(),
|
||||||
],
|
})
|
||||||
)
|
type OldValueSpecNumber = z.infer<typeof matchOldValueSpecNumber>
|
||||||
|
|
||||||
export const matchOldValueSpecNumber = object(
|
export const matchOldValueSpecBoolean = z.object({
|
||||||
{
|
type: z.enum(["boolean"]),
|
||||||
type: literals("number"),
|
default: z.boolean(),
|
||||||
nullable: boolean,
|
name: z.string(),
|
||||||
name: string,
|
description: z.string().nullable().optional(),
|
||||||
range: string,
|
warning: z.string().nullable().optional(),
|
||||||
integral: boolean,
|
})
|
||||||
default: number,
|
type OldValueSpecBoolean = z.infer<typeof matchOldValueSpecBoolean>
|
||||||
description: string,
|
|
||||||
warning: string,
|
type OldValueSpecObject = {
|
||||||
units: string,
|
type: "object"
|
||||||
placeholder: anyOf(number, string),
|
spec: OldConfigSpec
|
||||||
},
|
name: string
|
||||||
["default", "description", "warning", "units", "placeholder"],
|
description?: string | null
|
||||||
)
|
warning?: string | null
|
||||||
type OldValueSpecNumber = typeof matchOldValueSpecNumber._TYPE
|
}
|
||||||
|
const matchOldValueSpecObject: z.ZodType<OldValueSpecObject> = z.object({
|
||||||
export const matchOldValueSpecBoolean = object(
|
type: z.enum(["object"]),
|
||||||
{
|
spec: z.lazy(() => matchOldConfigSpec),
|
||||||
type: literals("boolean"),
|
name: z.string(),
|
||||||
default: boolean,
|
description: z.string().nullable().optional(),
|
||||||
name: string,
|
warning: z.string().nullable().optional(),
|
||||||
description: string,
|
})
|
||||||
warning: string,
|
|
||||||
},
|
const matchOldValueSpecEnum = z.object({
|
||||||
["description", "warning"],
|
values: z.array(z.string()),
|
||||||
)
|
"value-names": z.record(z.string(), z.string()),
|
||||||
type OldValueSpecBoolean = typeof matchOldValueSpecBoolean._TYPE
|
type: z.enum(["enum"]),
|
||||||
|
default: z.string(),
|
||||||
const matchOldValueSpecObject = object(
|
name: z.string(),
|
||||||
{
|
description: z.string().nullable().optional(),
|
||||||
type: literals("object"),
|
warning: z.string().nullable().optional(),
|
||||||
spec: _matchOldConfigSpec,
|
})
|
||||||
name: string,
|
type OldValueSpecEnum = z.infer<typeof matchOldValueSpecEnum>
|
||||||
description: string,
|
|
||||||
warning: string,
|
const matchOldUnionTagSpec = z.object({
|
||||||
},
|
id: z.string(), // The name of the field containing one of the union variants
|
||||||
["description", "warning"],
|
"variant-names": z.record(z.string(), z.string()), // The name of each variant
|
||||||
)
|
name: z.string(),
|
||||||
type OldValueSpecObject = typeof matchOldValueSpecObject._TYPE
|
description: z.string().nullable().optional(),
|
||||||
|
warning: z.string().nullable().optional(),
|
||||||
const matchOldValueSpecEnum = object(
|
})
|
||||||
{
|
type OldValueSpecUnion = {
|
||||||
values: array(string),
|
type: "union"
|
||||||
"value-names": dictionary([string, string]),
|
tag: z.infer<typeof matchOldUnionTagSpec>
|
||||||
type: literals("enum"),
|
variants: Record<string, OldConfigSpec>
|
||||||
default: string,
|
default: string
|
||||||
name: string,
|
}
|
||||||
description: string,
|
const matchOldValueSpecUnion: z.ZodType<OldValueSpecUnion> = z.object({
|
||||||
warning: string,
|
type: z.enum(["union"]),
|
||||||
},
|
tag: matchOldUnionTagSpec,
|
||||||
["description", "warning"],
|
variants: z.record(
|
||||||
)
|
z.string(),
|
||||||
type OldValueSpecEnum = typeof matchOldValueSpecEnum._TYPE
|
z.lazy(() => matchOldConfigSpec),
|
||||||
|
),
|
||||||
const matchOldUnionTagSpec = object(
|
default: z.string(),
|
||||||
{
|
|
||||||
id: string, // The name of the field containing one of the union variants
|
|
||||||
"variant-names": dictionary([string, string]), // The name of each variant
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
warning: string,
|
|
||||||
},
|
|
||||||
["description", "warning"],
|
|
||||||
)
|
|
||||||
const matchOldValueSpecUnion = object({
|
|
||||||
type: literals("union"),
|
|
||||||
tag: matchOldUnionTagSpec,
|
|
||||||
variants: dictionary([string, _matchOldConfigSpec]),
|
|
||||||
default: string,
|
|
||||||
})
|
})
|
||||||
type OldValueSpecUnion = typeof matchOldValueSpecUnion._TYPE
|
|
||||||
|
|
||||||
const [matchOldUniqueBy, setOldUniqueBy] = deferred<OldUniqueBy>()
|
|
||||||
type OldUniqueBy =
|
type OldUniqueBy =
|
||||||
| null
|
| null
|
||||||
| string
|
| string
|
||||||
| { any: OldUniqueBy[] }
|
| { any: OldUniqueBy[] }
|
||||||
| { all: OldUniqueBy[] }
|
| { all: OldUniqueBy[] }
|
||||||
|
|
||||||
setOldUniqueBy(
|
const matchOldUniqueBy: z.ZodType<OldUniqueBy> = z.lazy(() =>
|
||||||
anyOf(
|
z.union([
|
||||||
nill,
|
z.null(),
|
||||||
string,
|
z.string(),
|
||||||
object({ any: array(matchOldUniqueBy) }),
|
z.object({ any: z.array(matchOldUniqueBy) }),
|
||||||
object({ all: array(matchOldUniqueBy) }),
|
z.object({ all: z.array(matchOldUniqueBy) }),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
type OldListValueSpecObject = {
|
||||||
|
spec: OldConfigSpec
|
||||||
|
"unique-by"?: OldUniqueBy | null
|
||||||
|
"display-as"?: string | null
|
||||||
|
}
|
||||||
|
const matchOldListValueSpecObject: z.ZodType<OldListValueSpecObject> = z.object(
|
||||||
|
{
|
||||||
|
spec: z.lazy(() => matchOldConfigSpec), // this is a mapped type of the config object at this level, replacing the object's values with specs on those values
|
||||||
|
"unique-by": matchOldUniqueBy.nullable().optional(), // indicates whether duplicates can be permitted in the list
|
||||||
|
"display-as": z.string().nullable().optional(), // this should be a handlebars template which can make use of the entire config which corresponds to 'spec'
|
||||||
|
},
|
||||||
|
)
|
||||||
|
type OldListValueSpecUnion = {
|
||||||
|
"unique-by"?: OldUniqueBy | null
|
||||||
|
"display-as"?: string | null
|
||||||
|
tag: z.infer<typeof matchOldUnionTagSpec>
|
||||||
|
variants: Record<string, OldConfigSpec>
|
||||||
|
}
|
||||||
|
const matchOldListValueSpecUnion: z.ZodType<OldListValueSpecUnion> = z.object({
|
||||||
|
"unique-by": matchOldUniqueBy.nullable().optional(),
|
||||||
|
"display-as": z.string().nullable().optional(),
|
||||||
|
tag: matchOldUnionTagSpec,
|
||||||
|
variants: z.record(
|
||||||
|
z.string(),
|
||||||
|
z.lazy(() => matchOldConfigSpec),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
|
|
||||||
const matchOldListValueSpecObject = object(
|
|
||||||
{
|
|
||||||
spec: _matchOldConfigSpec, // this is a mapped type of the config object at this level, replacing the object's values with specs on those values
|
|
||||||
"unique-by": matchOldUniqueBy, // indicates whether duplicates can be permitted in the list
|
|
||||||
"display-as": string, // this should be a handlebars template which can make use of the entire config which corresponds to 'spec'
|
|
||||||
},
|
|
||||||
["display-as", "unique-by"],
|
|
||||||
)
|
|
||||||
const matchOldListValueSpecString = object(
|
|
||||||
{
|
|
||||||
masked: boolean,
|
|
||||||
copyable: boolean,
|
|
||||||
pattern: string,
|
|
||||||
"pattern-description": string,
|
|
||||||
placeholder: string,
|
|
||||||
},
|
|
||||||
["pattern", "pattern-description", "placeholder", "copyable", "masked"],
|
|
||||||
)
|
|
||||||
|
|
||||||
const matchOldListValueSpecEnum = object({
|
|
||||||
values: array(string),
|
|
||||||
"value-names": dictionary([string, string]),
|
|
||||||
})
|
})
|
||||||
const matchOldListValueSpecNumber = object(
|
const matchOldListValueSpecString = z.object({
|
||||||
{
|
masked: z.boolean().nullable().optional(),
|
||||||
range: string,
|
copyable: z.boolean().nullable().optional(),
|
||||||
integral: boolean,
|
pattern: z.string().nullable().optional(),
|
||||||
units: string,
|
"pattern-description": z.string().nullable().optional(),
|
||||||
placeholder: anyOf(number, string),
|
placeholder: z.string().nullable().optional(),
|
||||||
},
|
})
|
||||||
["units", "placeholder"],
|
|
||||||
)
|
const matchOldListValueSpecEnum = z.object({
|
||||||
|
values: z.array(z.string()),
|
||||||
|
"value-names": z.record(z.string(), z.string()),
|
||||||
|
})
|
||||||
|
const matchOldListValueSpecNumber = z.object({
|
||||||
|
range: z.string(),
|
||||||
|
integral: z.boolean(),
|
||||||
|
units: z.string().nullable().optional(),
|
||||||
|
placeholder: z.union([z.number(), z.string()]).nullable().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
type OldValueSpecListBase = {
|
||||||
|
type: "list"
|
||||||
|
range: string
|
||||||
|
default: string[] | number[] | OldDefaultString[] | Record<string, unknown>[]
|
||||||
|
name: string
|
||||||
|
description?: string | null
|
||||||
|
warning?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type OldValueSpecList = OldValueSpecListBase &
|
||||||
|
(
|
||||||
|
| { subtype: "string"; spec: z.infer<typeof matchOldListValueSpecString> }
|
||||||
|
| { subtype: "enum"; spec: z.infer<typeof matchOldListValueSpecEnum> }
|
||||||
|
| { subtype: "object"; spec: OldListValueSpecObject }
|
||||||
|
| { subtype: "number"; spec: z.infer<typeof matchOldListValueSpecNumber> }
|
||||||
|
| { subtype: "union"; spec: OldListValueSpecUnion }
|
||||||
|
)
|
||||||
|
|
||||||
// represents a spec for a list
|
// represents a spec for a list
|
||||||
const matchOldValueSpecList = every(
|
export const matchOldValueSpecList: z.ZodType<OldValueSpecList> =
|
||||||
object(
|
z.intersection(
|
||||||
{
|
z.object({
|
||||||
type: literals("list"),
|
type: z.enum(["list"]),
|
||||||
range: string, // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules
|
range: z.string(), // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules
|
||||||
default: anyOf(
|
default: z.union([
|
||||||
array(string),
|
z.array(z.string()),
|
||||||
array(number),
|
z.array(z.number()),
|
||||||
array(matchOldDefaultString),
|
z.array(matchOldDefaultString),
|
||||||
array(object),
|
z.array(z.object({}).passthrough()),
|
||||||
),
|
]),
|
||||||
name: string,
|
name: z.string(),
|
||||||
description: string,
|
description: z.string().nullable().optional(),
|
||||||
warning: string,
|
warning: z.string().nullable().optional(),
|
||||||
},
|
|
||||||
["description", "warning"],
|
|
||||||
),
|
|
||||||
anyOf(
|
|
||||||
object({
|
|
||||||
subtype: literals("string"),
|
|
||||||
spec: matchOldListValueSpecString,
|
|
||||||
}),
|
}),
|
||||||
object({
|
z.union([
|
||||||
subtype: literals("enum"),
|
z.object({
|
||||||
spec: matchOldListValueSpecEnum,
|
subtype: z.enum(["string"]),
|
||||||
}),
|
spec: matchOldListValueSpecString,
|
||||||
object({
|
}),
|
||||||
subtype: literals("object"),
|
z.object({
|
||||||
spec: matchOldListValueSpecObject,
|
subtype: z.enum(["enum"]),
|
||||||
}),
|
spec: matchOldListValueSpecEnum,
|
||||||
object({
|
}),
|
||||||
subtype: literals("number"),
|
z.object({
|
||||||
spec: matchOldListValueSpecNumber,
|
subtype: z.enum(["object"]),
|
||||||
}),
|
spec: matchOldListValueSpecObject,
|
||||||
),
|
}),
|
||||||
)
|
z.object({
|
||||||
type OldValueSpecList = typeof matchOldValueSpecList._TYPE
|
subtype: z.enum(["number"]),
|
||||||
|
spec: matchOldListValueSpecNumber,
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subtype: z.enum(["union"]),
|
||||||
|
spec: matchOldListValueSpecUnion,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
) as unknown as z.ZodType<OldValueSpecList>
|
||||||
|
|
||||||
const matchOldValueSpecPointer = every(
|
type OldValueSpecPointer = {
|
||||||
object({
|
type: "pointer"
|
||||||
type: literal("pointer"),
|
} & (
|
||||||
|
| {
|
||||||
|
subtype: "package"
|
||||||
|
target: "tor-key" | "tor-address" | "lan-address"
|
||||||
|
"package-id": string
|
||||||
|
interface: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
subtype: "package"
|
||||||
|
target: "config"
|
||||||
|
"package-id": string
|
||||||
|
selector: string
|
||||||
|
multi: boolean
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const matchOldValueSpecPointer: z.ZodType<OldValueSpecPointer> = z.intersection(
|
||||||
|
z.object({
|
||||||
|
type: z.literal("pointer"),
|
||||||
}),
|
}),
|
||||||
anyOf(
|
z.union([
|
||||||
object({
|
z.object({
|
||||||
subtype: literal("package"),
|
subtype: z.literal("package"),
|
||||||
target: literals("tor-key", "tor-address", "lan-address"),
|
target: z.enum(["tor-key", "tor-address", "lan-address"]),
|
||||||
"package-id": string,
|
"package-id": z.string(),
|
||||||
interface: string,
|
interface: z.string(),
|
||||||
}),
|
}),
|
||||||
object({
|
z.object({
|
||||||
subtype: literal("package"),
|
subtype: z.literal("package"),
|
||||||
target: literals("config"),
|
target: z.enum(["config"]),
|
||||||
"package-id": string,
|
"package-id": z.string(),
|
||||||
selector: string,
|
selector: z.string(),
|
||||||
multi: boolean,
|
multi: z.boolean(),
|
||||||
}),
|
}),
|
||||||
),
|
]),
|
||||||
)
|
) as unknown as z.ZodType<OldValueSpecPointer>
|
||||||
type OldValueSpecPointer = typeof matchOldValueSpecPointer._TYPE
|
|
||||||
|
|
||||||
export const matchOldValueSpec = anyOf(
|
type OldValueSpecString = z.infer<typeof matchOldValueSpecString>
|
||||||
|
|
||||||
|
type OldValueSpec =
|
||||||
|
| OldValueSpecString
|
||||||
|
| OldValueSpecNumber
|
||||||
|
| OldValueSpecBoolean
|
||||||
|
| OldValueSpecObject
|
||||||
|
| OldValueSpecEnum
|
||||||
|
| OldValueSpecList
|
||||||
|
| OldValueSpecUnion
|
||||||
|
| OldValueSpecPointer
|
||||||
|
|
||||||
|
export const matchOldValueSpec: z.ZodType<OldValueSpec> = z.union([
|
||||||
matchOldValueSpecString,
|
matchOldValueSpecString,
|
||||||
matchOldValueSpecNumber,
|
matchOldValueSpecNumber,
|
||||||
matchOldValueSpecBoolean,
|
matchOldValueSpecBoolean,
|
||||||
matchOldValueSpecObject,
|
matchOldValueSpecObject as z.ZodType<OldValueSpecObject>,
|
||||||
matchOldValueSpecEnum,
|
matchOldValueSpecEnum,
|
||||||
matchOldValueSpecList,
|
matchOldValueSpecList as z.ZodType<OldValueSpecList>,
|
||||||
matchOldValueSpecUnion,
|
matchOldValueSpecUnion as z.ZodType<OldValueSpecUnion>,
|
||||||
matchOldValueSpecPointer,
|
matchOldValueSpecPointer as z.ZodType<OldValueSpecPointer>,
|
||||||
)
|
])
|
||||||
type OldValueSpec = typeof matchOldValueSpec._TYPE
|
|
||||||
|
|
||||||
setMatchOldConfigSpec(dictionary([string, matchOldValueSpec]))
|
|
||||||
|
|
||||||
export class Range {
|
export class Range {
|
||||||
min?: number
|
min?: number
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { System } from "../../Interfaces/System"
|
import { System } from "../../Interfaces/System"
|
||||||
import { Effects } from "../../Models/Effects"
|
import { Effects } from "../../Models/Effects"
|
||||||
import { T, utils } from "@start9labs/start-sdk"
|
import { ExtendedVersion, T, utils, VersionRange } from "@start9labs/start-sdk"
|
||||||
import { Optional } from "ts-matches/lib/parsers/interfaces"
|
|
||||||
|
|
||||||
export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js"
|
export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js"
|
||||||
|
|
||||||
@@ -11,6 +10,7 @@ type RunningMain = {
|
|||||||
|
|
||||||
export class SystemForStartOs implements System {
|
export class SystemForStartOs implements System {
|
||||||
private runningMain: RunningMain | undefined
|
private runningMain: RunningMain | undefined
|
||||||
|
private starting: boolean = false
|
||||||
|
|
||||||
static of() {
|
static of() {
|
||||||
return new SystemForStartOs(require(STARTOS_JS_LOCATION))
|
return new SystemForStartOs(require(STARTOS_JS_LOCATION))
|
||||||
@@ -19,22 +19,23 @@ export class SystemForStartOs implements System {
|
|||||||
constructor(readonly abi: T.ABI) {
|
constructor(readonly abi: T.ABI) {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
async containerInit(effects: Effects): Promise<void> {
|
|
||||||
return void (await this.abi.containerInit({ effects }))
|
async init(
|
||||||
}
|
|
||||||
async packageInit(
|
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
|
kind: "install" | "update" | "restore" | null,
|
||||||
|
): Promise<void> {
|
||||||
|
return void (await this.abi.init({ effects, kind }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async exit(
|
||||||
|
effects: Effects,
|
||||||
|
target: ExtendedVersion | VersionRange | null,
|
||||||
timeoutMs: number | null = null,
|
timeoutMs: number | null = null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return void (await this.abi.packageInit({ effects }))
|
await this.stop()
|
||||||
}
|
return void (await this.abi.uninit({ effects, target }))
|
||||||
async packageUninit(
|
|
||||||
effects: Effects,
|
|
||||||
nextVersion: Optional<string> = null,
|
|
||||||
timeoutMs: number | null = null,
|
|
||||||
): Promise<void> {
|
|
||||||
return void (await this.abi.packageUninit({ effects, nextVersion }))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createBackup(
|
async createBackup(
|
||||||
effects: T.Effects,
|
effects: T.Effects,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
@@ -43,22 +44,15 @@ export class SystemForStartOs implements System {
|
|||||||
effects,
|
effects,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
async restoreBackup(
|
|
||||||
effects: T.Effects,
|
|
||||||
timeoutMs: number | null,
|
|
||||||
): Promise<void> {
|
|
||||||
return void (await this.abi.restoreBackup({
|
|
||||||
effects,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
getActionInput(
|
getActionInput(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
id: string,
|
id: string,
|
||||||
|
prefill: Record<string, unknown> | null,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<T.ActionInput | null> {
|
): Promise<T.ActionInput | null> {
|
||||||
const action = this.abi.actions.get(id)
|
const action = this.abi.actions.get(id)
|
||||||
if (!action) throw new Error(`Action ${id} not found`)
|
if (!action) throw new Error(`Action ${id} not found`)
|
||||||
return action.getInput({ effects })
|
return action.getInput({ effects, prefill })
|
||||||
}
|
}
|
||||||
runAction(
|
runAction(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
@@ -71,28 +65,27 @@ export class SystemForStartOs implements System {
|
|||||||
return action.run({ effects, input })
|
return action.run({ effects, input })
|
||||||
}
|
}
|
||||||
|
|
||||||
async exit(): Promise<void> {}
|
|
||||||
|
|
||||||
async start(effects: Effects): Promise<void> {
|
async start(effects: Effects): Promise<void> {
|
||||||
if (this.runningMain) return
|
try {
|
||||||
effects.constRetry = utils.once(() => effects.restart())
|
if (this.runningMain || this.starting) return
|
||||||
let mainOnTerm: () => Promise<void> | undefined
|
this.starting = true
|
||||||
const started = async (onTerm: () => Promise<void>) => {
|
effects.constRetry = utils.once(() => {
|
||||||
await effects.setMainStatus({ status: "running" })
|
console.debug(".const() triggered")
|
||||||
mainOnTerm = onTerm
|
effects.restart()
|
||||||
return null
|
|
||||||
}
|
|
||||||
const daemons = await (
|
|
||||||
await this.abi.main({
|
|
||||||
effects,
|
|
||||||
started,
|
|
||||||
})
|
})
|
||||||
).build()
|
let mainOnTerm: () => Promise<void> | undefined
|
||||||
this.runningMain = {
|
const daemons = await (
|
||||||
stop: async () => {
|
await this.abi.main({
|
||||||
if (mainOnTerm) await mainOnTerm()
|
effects,
|
||||||
await daemons.term()
|
})
|
||||||
},
|
).build()
|
||||||
|
this.runningMain = {
|
||||||
|
stop: async () => {
|
||||||
|
await daemons.term()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.starting = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { types as T } from "@start9labs/start-sdk"
|
import {
|
||||||
|
ExtendedVersion,
|
||||||
|
types as T,
|
||||||
|
VersionRange,
|
||||||
|
} from "@start9labs/start-sdk"
|
||||||
import { Effects } from "../Models/Effects"
|
import { Effects } from "../Models/Effects"
|
||||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||||
import { Optional } from "ts-matches/lib/parsers/interfaces"
|
|
||||||
|
|
||||||
export type Procedure =
|
export type Procedure =
|
||||||
| "/packageInit"
|
|
||||||
| "/packageUninit"
|
|
||||||
| "/backup/create"
|
| "/backup/create"
|
||||||
| "/backup/restore"
|
|
||||||
| `/actions/${string}/getInput`
|
| `/actions/${string}/getInput`
|
||||||
| `/actions/${string}/run`
|
| `/actions/${string}/run`
|
||||||
|
|
||||||
@@ -15,20 +15,15 @@ export type ExecuteResult =
|
|||||||
| { ok: unknown }
|
| { ok: unknown }
|
||||||
| { err: { code: number; message: string } }
|
| { err: { code: number; message: string } }
|
||||||
export type System = {
|
export type System = {
|
||||||
containerInit(effects: T.Effects): Promise<void>
|
init(
|
||||||
|
effects: T.Effects,
|
||||||
|
kind: "install" | "update" | "restore" | null,
|
||||||
|
): Promise<void>
|
||||||
|
|
||||||
start(effects: T.Effects): Promise<void>
|
start(effects: T.Effects): Promise<void>
|
||||||
stop(): Promise<void>
|
stop(): Promise<void>
|
||||||
|
|
||||||
packageInit(effects: Effects, timeoutMs: number | null): Promise<void>
|
|
||||||
packageUninit(
|
|
||||||
effects: Effects,
|
|
||||||
nextVersion: Optional<string>,
|
|
||||||
timeoutMs: number | null,
|
|
||||||
): Promise<void>
|
|
||||||
|
|
||||||
createBackup(effects: T.Effects, timeoutMs: number | null): Promise<void>
|
createBackup(effects: T.Effects, timeoutMs: number | null): Promise<void>
|
||||||
restoreBackup(effects: T.Effects, timeoutMs: number | null): Promise<void>
|
|
||||||
runAction(
|
runAction(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
actionId: string,
|
actionId: string,
|
||||||
@@ -38,10 +33,14 @@ export type System = {
|
|||||||
getActionInput(
|
getActionInput(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
actionId: string,
|
actionId: string,
|
||||||
|
prefill: Record<string, unknown> | null,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<T.ActionInput | null>
|
): Promise<T.ActionInput | null>
|
||||||
|
|
||||||
exit(): Promise<void>
|
exit(
|
||||||
|
effects: Effects,
|
||||||
|
target: ExtendedVersion | VersionRange | null,
|
||||||
|
): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RunningMain = {
|
export type RunningMain = {
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export class CallbackHolder {
|
|||||||
constructor(private effects?: T.Effects) {}
|
constructor(private effects?: T.Effects) {}
|
||||||
|
|
||||||
private callbacks = new Map<number, Function>()
|
private callbacks = new Map<number, Function>()
|
||||||
private children: WeakRef<CallbackHolder>[] = []
|
private onLeaveContextCallbacks: Function[] = []
|
||||||
|
private children: Map<string, CallbackHolder> = new Map()
|
||||||
private newId() {
|
private newId() {
|
||||||
return CallbackIdCell.inc++
|
return CallbackIdCell.inc++
|
||||||
}
|
}
|
||||||
@@ -32,23 +33,30 @@ export class CallbackHolder {
|
|||||||
})
|
})
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
child(): CallbackHolder {
|
child(name: string): CallbackHolder {
|
||||||
const child = new CallbackHolder()
|
this.removeChild(name)
|
||||||
this.children.push(new WeakRef(child))
|
const child = new CallbackHolder(this.effects)
|
||||||
|
this.children.set(name, child)
|
||||||
return child
|
return child
|
||||||
}
|
}
|
||||||
removeChild(child: CallbackHolder) {
|
|
||||||
this.children = this.children.filter((c) => {
|
getChild(name: string): CallbackHolder | null {
|
||||||
const ref = c.deref()
|
return this.children.get(name) || null
|
||||||
return ref && ref !== child
|
}
|
||||||
})
|
|
||||||
|
removeChild(name: string) {
|
||||||
|
const child = this.children.get(name)
|
||||||
|
if (child) {
|
||||||
|
child.leaveContext()
|
||||||
|
this.children.delete(name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private getCallback(index: number): Function | undefined {
|
private getCallback(index: number): Function | undefined {
|
||||||
let callback = this.callbacks.get(index)
|
let callback = this.callbacks.get(index)
|
||||||
if (callback) this.callbacks.delete(index)
|
if (callback) this.callbacks.delete(index)
|
||||||
else {
|
else {
|
||||||
for (let i = 0; i < this.children.length; i++) {
|
for (let [_, child] of this.children) {
|
||||||
callback = this.children[i].deref()?.getCallback(index)
|
callback = child.getCallback(index)
|
||||||
if (callback) return callback
|
if (callback) return callback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,6 +65,25 @@ export class CallbackHolder {
|
|||||||
callCallback(index: number, args: any[]): Promise<unknown> {
|
callCallback(index: number, args: any[]): Promise<unknown> {
|
||||||
const callback = this.getCallback(index)
|
const callback = this.getCallback(index)
|
||||||
if (!callback) return Promise.resolve()
|
if (!callback) return Promise.resolve()
|
||||||
return Promise.resolve().then(() => callback(...args))
|
return Promise.resolve()
|
||||||
|
.then(() => callback(...args))
|
||||||
|
.catch((e) => console.error("callback failed", e))
|
||||||
|
}
|
||||||
|
onLeaveContext(fn: Function) {
|
||||||
|
this.onLeaveContextCallbacks.push(fn)
|
||||||
|
}
|
||||||
|
leaveContext() {
|
||||||
|
for (let [_, child] of this.children) {
|
||||||
|
child.leaveContext()
|
||||||
|
}
|
||||||
|
this.children = new Map()
|
||||||
|
for (let fn of this.onLeaveContextCallbacks) {
|
||||||
|
try {
|
||||||
|
fn()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.onLeaveContextCallbacks = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,19 @@
|
|||||||
import {
|
import { z } from "@start9labs/start-sdk"
|
||||||
object,
|
|
||||||
literal,
|
|
||||||
string,
|
|
||||||
boolean,
|
|
||||||
array,
|
|
||||||
dictionary,
|
|
||||||
literals,
|
|
||||||
number,
|
|
||||||
Parser,
|
|
||||||
some,
|
|
||||||
} from "ts-matches"
|
|
||||||
import { matchDuration } from "./Duration"
|
import { matchDuration } from "./Duration"
|
||||||
|
|
||||||
const VolumeId = string
|
export const matchDockerProcedure = z.object({
|
||||||
const Path = string
|
type: z.literal("docker"),
|
||||||
|
image: z.string(),
|
||||||
|
system: z.boolean().optional(),
|
||||||
|
entrypoint: z.string(),
|
||||||
|
args: z.array(z.string()).default([]),
|
||||||
|
mounts: z.record(z.string(), z.string()).optional(),
|
||||||
|
"io-format": z
|
||||||
|
.enum(["json", "json-pretty", "yaml", "cbor", "toml", "toml-pretty"])
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
"sigterm-timeout": z.union([z.number(), matchDuration]).catch(30),
|
||||||
|
inject: z.boolean().default(false),
|
||||||
|
})
|
||||||
|
|
||||||
export type VolumeId = string
|
export type DockerProcedure = z.infer<typeof matchDockerProcedure>
|
||||||
export type Path = string
|
|
||||||
export const matchDockerProcedure = object(
|
|
||||||
{
|
|
||||||
type: literal("docker"),
|
|
||||||
image: string,
|
|
||||||
system: boolean,
|
|
||||||
entrypoint: string,
|
|
||||||
args: array(string),
|
|
||||||
mounts: dictionary([VolumeId, Path]),
|
|
||||||
"io-format": literals(
|
|
||||||
"json",
|
|
||||||
"json-pretty",
|
|
||||||
"yaml",
|
|
||||||
"cbor",
|
|
||||||
"toml",
|
|
||||||
"toml-pretty",
|
|
||||||
),
|
|
||||||
"sigterm-timeout": some(number, matchDuration),
|
|
||||||
inject: boolean,
|
|
||||||
},
|
|
||||||
["io-format", "sigterm-timeout", "system", "args", "inject", "mounts"],
|
|
||||||
{
|
|
||||||
"sigterm-timeout": 30,
|
|
||||||
inject: false,
|
|
||||||
args: [],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export type DockerProcedure = typeof matchDockerProcedure._TYPE
|
|
||||||
|
|||||||