init: OffScale — standalone MIT Tailscale/Headscale client
Extracted from the officer-suite monorepo into its own public repo. The app is
open source and free; the rest of the suite is not, so it could not stay in a
tree that ships an account layer.
What moved:
- apps/offscale/* -> repo root (App shell, app.json, assets)
- packages/officer-net/ -> modules/officer-net/, a `file:` dependency
- packages/core VPN closure -> src/vpn/ (screen, hook, native bridge, invite)
- packages/core shared primitives -> src/ui/ (theme, cards, tabs, backdrop, splash)
- scripts/build-{ios,android}.sh -> scripts/, cut down to this one app
What deliberately did NOT move: the platform account layer. VpnScreen used to
render core's AppSettingsScreen, which drags in ServersSection -> servers.ts,
useAuth, api.ts, shared-session, install-guard, api-keys. None of that exists
here. src/settings/SettingsScreen.tsx is a fresh replacement rendering exactly
what VpnScreen passed it — the app's own sections plus About — with no Servers
tab, no Appearance control (the app is pinned to dark) and no sign-out.
The one platform reference left in the closure was a dead `import { request }
from './api'` in officer-net.ts, unused since enrollVpn() was removed. Gone. The
app now contacts nothing but the control server you point it at, its peers, and
the enrolment sidecar named inside an invite link you tapped.
Dependencies are pinned to the exact versions the monorepo has installed, not to
ranges: this repo starts from versions already proven to build, and an unpinned
install is a supply-chain decision nobody made. react-query, react-dom,
react-native-screens, expo-secure-store, track-player, expo-audio and
expo-file-system were all declared by the old app and imported by none of this
code, so they are not here.
NOTICE carries Tailscale's BSD-3 text, names the built artifacts that contain
their compiled code, and treats the trademark question separately — including
that src/ui/TailscaleMark.tsx reproduces their logo in the attribution strip.
This commit is contained in:
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Clean-build the release APK and install it on the attached device.
|
||||
#
|
||||
# scripts/build-android.sh # build + install
|
||||
# ABIS=arm64-v8a,x86_64 scripts/… # add an emulator ABI
|
||||
# MINIFY=1 scripts/… # turn R8 on (see the note below — not the default)
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
LOGS="${TMPDIR:-/tmp}offscale-build"
|
||||
mkdir -p "$LOGS"
|
||||
log="$LOGS/android.log"
|
||||
: >"$log"
|
||||
cd "$ROOT" || exit 1
|
||||
|
||||
# The vendored Tailscale core is built by hand, per platform, and forgetting it fails much later as a
|
||||
# missing symbol rather than a missing rebuild — Android says "Unresolved reference 'loginTo'" at
|
||||
# Kotlin compile time. Do it here instead of relying on anyone remembering.
|
||||
. "$ROOT/scripts/ensure-core.sh" android
|
||||
|
||||
echo "logs: $log"
|
||||
|
||||
# `bun run prebuild`, NOT `bunx expo prebuild`: the package.json script runs gen-adaptive-icon.sh
|
||||
# first, and calling expo directly silently skips it — which ships an icon the Android launcher mask
|
||||
# crops. Trailing args append to that script, and `expo prebuild` is its last command.
|
||||
if ! bun run prebuild --platform android --clean --no-install >>"$log" 2>&1; then
|
||||
echo "PREBUILD FAILED — $log"; exit 1
|
||||
fi
|
||||
|
||||
# Build for phones only.
|
||||
#
|
||||
# Expo's generated gradle.properties asks for `armeabi-v7a,arm64-v8a,x86,x86_64`, and the native
|
||||
# libraries are most of an APK — for an app this shape, roughly 65 MB of it is lib/x86 and lib/x86_64,
|
||||
# Intel builds for emulators that no phone will ever execute, and another 21 MB is armeabi-v7a for
|
||||
# 32-bit devices. The Tailscale core is a large .so and gets built once per ABI, so this matters more
|
||||
# here than in an app without one.
|
||||
#
|
||||
# Overridden here rather than in gradle.properties because `expo prebuild --clean` on the line above
|
||||
# regenerates that file every single build, so an edit there survives exactly nothing.
|
||||
#
|
||||
# ABIS=… to override — an emulator needs x86_64 and will refuse an APK without it.
|
||||
ABIS="${ABIS:-arm64-v8a}"
|
||||
|
||||
# MINIFY=1 turns R8 on. OFF by default, and that default is the honest one.
|
||||
#
|
||||
# R8 deletes every class nothing reaches from an entry point. It decides "nothing reaches this" by
|
||||
# reading the code — so anything reached by REFLECTION looks dead and gets deleted. React Native and
|
||||
# Expo both do that, and so does the gomobile binding layer. The build still succeeds; the app crashes
|
||||
# on the device. This cannot be verified by looking at a file size, and it must not become the default
|
||||
# until a build with it has been opened on a real phone, connected to a tailnet, and used.
|
||||
MINIFY_ARGS=()
|
||||
if [[ "${MINIFY:-0}" == "1" ]]; then
|
||||
MINIFY_ARGS=(-Pandroid.enableMinifyInReleaseBuilds=true)
|
||||
fi
|
||||
|
||||
# assembleRelease, always. A debug build needs Metro attached and behaves differently from what ships,
|
||||
# so testing it answers the wrong question.
|
||||
if ! (cd android && ./gradlew :app:assembleRelease -PreactNativeArchitectures="$ABIS" \
|
||||
"${MINIFY_ARGS[@]}" --console=plain) >>"$log" 2>&1; then
|
||||
echo "GRADLE FAILED — $log"; exit 1
|
||||
fi
|
||||
|
||||
apk="$(find android/app/build/outputs/apk/release -name '*.apk' | head -1)"
|
||||
[[ -z "$apk" ]] && { echo "NO APK"; exit 1; }
|
||||
|
||||
if ! adb install -r "$apk" >>"$log" 2>&1; then
|
||||
# Almost always the cable, not the build. The APK is on disk either way: rerun just the install
|
||||
# rather than the whole thing.
|
||||
echo "INSTALL FAILED (device attached?) — apk at $apk"; exit 1
|
||||
fi
|
||||
echo "OK"
|
||||
Reference in New Issue
Block a user