1+ #! /usr/bin/env bash
2+ set -euo pipefail
3+
4+ # The instance id is used to namespace the dev environment to allow for multiple instances to run
5+ # without conflicts. e.g:
6+ # npm run dev start
7+ # UMBREL_DEV_INSTANCE='apps' npm run dev start
8+ #
9+ # Will spin up two separate umbrel-dev instances accessible at:
10+ # http://umbrel-dev.local
11+ # http://umbrel-dev-apps.local
12+ INSTANCE_ID_PREFIX=" umbrel-dev"
13+ INSTANCE_ID=" ${INSTANCE_ID_PREFIX}${UMBREL_DEV_INSTANCE: +-$UMBREL_DEV_INSTANCE } "
14+
15+ show_help () {
16+ cat << EOF
17+ umbrel-dev
18+
19+ Automatically initialize and manage an umbrelOS development environment.
20+
21+ Usage: npm run dev <command> [-- <args>]
22+
23+ Commands:
24+ help Show this help message
25+ start Either start an existing dev environment or create and start a new one
26+ logs Stream umbreld logs
27+ shell Get a shell inside the running dev environment
28+ exec -- <command> Execute a command inside the running dev environment
29+ client -- <rpc> [<args>] Query the umbreld RPC server via a CLI client
30+ rebuild Rebuild the operating system image from source and reboot the dev environment into it
31+ restart Restart the dev environment
32+ stop Stop the dev environment
33+ reset Reset the dev environment to a fresh state
34+ destroy Destroy the dev environment
35+
36+ Environment Variables:
37+ UMBREL_DEV_INSTANCE The instance id of the dev environment. Allows running multiple instances of
38+ umbrel-dev in different namespaces.
39+
40+ Note: umbrel-dev requires a Docker environment that exposes container IPs to the host. This is how Docker
41+ natively works on Linux and can be done with OrbStack on macOS. On Windows this should work with WSL 2.
42+
43+ EOF
44+ }
45+
46+ build_os_image () {
47+ docker buildx build --load --file packages/os/umbrelos.Dockerfile --tag " ${INSTANCE_ID} " .
48+ }
49+
50+ create_instance () {
51+ # --privileged is needed for systemd to work inside the container.
52+ #
53+ # We mount a named volume namespaced to the instance id at /data to immitate
54+ # the data partition of a physical install.
55+ #
56+ # We mount the monorepo inside the container at /umbrel-dev as readonly. We
57+ # setup a writeable fs overlay later to allow the container to install dependencies
58+ # without modifying the hosts source code dir.
59+ #
60+ # --label "dev.orbstack.http-port=80" stops OrbStack from trying to guess which port
61+ # we're trying to expose which causes some weirdness since it often gets it wrong.
62+ #
63+ # --label "dev.orbstack.domains=${INSTANCE_ID}.local" makes the instance accessble at
64+ # umbrel-dev.local on OrbStack installs.
65+ #
66+ # /sbin/init kicks of systemd as the container entrypoint.
67+ docker run \
68+ --detach \
69+ --interactive \
70+ --tty \
71+ --privileged \
72+ --name " ${INSTANCE_ID} " \
73+ --hostname " ${INSTANCE_ID} " \
74+ --volume " ${INSTANCE_ID} :/data" \
75+ --volume " ${PWD} :/umbrel-dev:ro" \
76+ --label " dev.orbstack.http-port=80" \
77+ --label " dev.orbstack.domains=${INSTANCE_ID} .local" \
78+ " ${INSTANCE_ID} " \
79+ /sbin/init
80+ }
81+
82+ start_instance () {
83+ docker start " ${INSTANCE_ID} "
84+ }
85+
86+ exec_in_instance () {
87+ docker exec --interactive --tty " ${INSTANCE_ID} " " ${@ } "
88+ }
89+
90+ stop_instance () {
91+ # We first need to execute poweroff inside the instance so systemd gracefully stops services before we kill the container
92+ exec_in_instance poweroff
93+ docker stop " ${INSTANCE_ID} "
94+ }
95+
96+ remove_instance () {
97+ docker rm --force " ${INSTANCE_ID} "
98+ }
99+
100+ remove_volume () {
101+ docker volume rm " ${INSTANCE_ID} "
102+ }
103+
104+ get_instance_ip () {
105+ docker inspect --format ' {{ .NetworkSettings.IPAddress }}' " ${INSTANCE_ID} "
106+ }
107+
108+ # Get the command
109+ if [ -z ${1+x} ]; then
110+ command=" "
111+ else
112+ command=" $1 "
113+ fi
114+
115+ if [[ " ${command} " = " start" ]] || [[ " ${command} " = " " ]]
116+ then
117+ echo " Starting umbrel-dev instance..."
118+ if ! start_instance > /dev/null
119+ then
120+ echo " Instance not found, creating a new one..."
121+ if ! docker image inspect " ${INSTANCE_ID} " > /dev/null
122+ then
123+ build_os_image
124+ fi
125+ create_instance
126+ fi
127+ echo
128+ echo " umbrel-dev instance is booting up..."
129+
130+ # Stream systemd logs until boot has completed
131+ docker logs --tail 100 --follow " ${INSTANCE_ID} " 2> /dev/null &
132+ logs_pid=$!
133+ exec_in_instance systemctl is-active --wait multi-user.target > /dev/null|| true
134+ sleep 2
135+ kill " ${logs_pid} " || true
136+ wait
137+
138+ # Stream umbreld logs until web server is up
139+ docker exec " ${INSTANCE_ID} " journalctl --unit umbrel --follow --lines 100 --output cat 2> /dev/null &
140+ logs_pid=$!
141+ docker exec " ${INSTANCE_ID} " curl --silent --retry 300 --retry-delay 1 --retry-connrefused http://localhost > /dev/null 2>&1 || true
142+ sleep 0.1
143+ kill " ${logs_pid} " || true
144+ wait
145+
146+ # Done!
147+ cat << 'EOF '
148+
149+
150+ ,;###GGGGGGGGGGl#Sp
151+ ,##GGGlW""^' '`""%GGGG#S,
152+ ,#GGG" "lGG#o
153+ #GGl^ '$GG#
154+ ,#GGb \GGG,
155+ lGG" "GGG
156+ #GGGlGGGl##p,,p##lGGl##p,,p###ll##GGGG
157+ !GGGlW"""*GGGGGGG#""""WlGGGGG#W""*WGGGGS
158+ "" "^ '" ""
159+
160+ EOF
161+ echo " Your umbrel-dev instance is ready at:"
162+ echo
163+ echo " http://${INSTANCE_ID} .local"
164+ echo " http://$( get_instance_ip) "
165+
166+ exit
167+ fi
168+
169+ if [[ " ${command} " = " help" ]]
170+ then
171+ show_help
172+
173+ exit
174+ fi
175+
176+ if [[ " ${command} " = " shell" ]]
177+ then
178+ exec_in_instance bash
179+
180+ exit
181+ fi
182+
183+ if [[ " ${command} " = " exec" ]]
184+ then
185+ shift
186+ exec_in_instance " ${@ } "
187+
188+ exit
189+ fi
190+
191+ if [[ " ${command} " = " logs" ]]
192+ then
193+ exec_in_instance journalctl --unit umbrel --follow --lines 100 --output cat
194+
195+ exit
196+ fi
197+
198+ if [[ " ${command} " = " client" ]]
199+ then
200+ shift
201+ exec_in_instance npm --prefix /umbrel-dev/packages/umbreld run start -- client ${@ }
202+
203+ exit
204+ fi
205+
206+ if [[ " ${command} " = " rebuild" ]]
207+ then
208+ echo " Rebuilding the operating system image from source..."
209+ build_os_image
210+ echo " Restarting the dev environment with the new image..."
211+ stop_instance || true
212+ remove_instance || true
213+ create_instance
214+
215+ exit
216+ fi
217+
218+ if [[ " ${command} " = " destroy" ]]
219+ then
220+ echo " Destroying the dev environment..."
221+ remove_instance || true
222+ remove_volume || true
223+
224+ exit
225+ fi
226+
227+ if [[ " ${command} " = " reset" ]]
228+ then
229+ echo " Resetting the dev environment state..."
230+ stop_instance || true
231+ remove_instance || true
232+ remove_volume || true
233+ create_instance
234+
235+ exit
236+ fi
237+
238+ if [[ " ${command} " = " restart" ]]
239+ then
240+ echo " Restarting the dev environment..."
241+ stop_instance
242+ start_instance
243+
244+ exit
245+ fi
246+
247+ if [[ " ${command} " = " stop" ]]
248+ then
249+ echo " Stopping the dev environment..."
250+ stop_instance
251+
252+ exit
253+ fi
254+
255+ # This is a special command that runs directly inside the container to setup the environment
256+ # It is not intended to be run on the host machine!
257+ if [[ " ${command} " = " container-init" ]]
258+ then
259+ # Check if this is the first boot
260+ first_boot=false
261+ if [[ ! -d " /data/umbrel-dev-overlay" ]]
262+ then
263+ first_boot=true
264+ fi
265+
266+ # Setup fs overlay so we can write to the source code dir without modifying it on the host
267+ echo " Setting up fs overlay..."
268+ mkdir -p /data/umbrel-dev-overlay/upperdir
269+ mkdir -p /data/umbrel-dev-overlay/workdir
270+ mount -t overlay overlay -o lowerdir=/umbrel-dev,upperdir=/data/umbrel-dev-overlay/upperdir,workdir=/data/umbrel-dev-overlay/workdir /umbrel-dev || true
271+
272+ # If this is the first boot we should nuke node_modules if they exist so we get fresh Linux deps instead
273+ # of trying to reuse deps installed from the host. (causes issues with macos native deps)
274+ if [[ " ${first_boot} " = true ]]
275+ then
276+ echo " Nuking node_modules inherited from host..."
277+ rm -rf /umbrel-dev/packages/ui/node_modules || true
278+ rm -rf /umbrel-dev/packages/umbreld/node_modules || true
279+ fi
280+
281+ # Install dependencies
282+ echo " Installing dependencies..."
283+ npm --prefix /umbrel-dev/packages/umbreld install
284+ npm --prefix /umbrel-dev/packages/ui install
285+
286+ # Run umbreld and ui
287+ echo " Starting umbreld and ui..."
288+ npm --prefix /umbrel-dev/packages/umbreld run dev &
289+ CHOKIDAR_USEPOLLING=true npm --prefix /umbrel-dev/packages/ui run dev &
290+ wait
291+
292+ exit
293+ fi
294+
295+ show_help
296+ exit
0 commit comments