massive shrink on docker
This commit is contained in:
+30
-21
@@ -1,23 +1,29 @@
|
|||||||
# Node/Bun
|
# Node/Bun (match at any depth — Overlord-Server/node_modules etc.)
|
||||||
node_modules/
|
**/node_modules/
|
||||||
.next/
|
**/.next/
|
||||||
.cache/
|
**/.cache/
|
||||||
dist/
|
**/dist/
|
||||||
.tmp/
|
**/.tmp/
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
*.db
|
**/*.db
|
||||||
*.db-journal
|
**/*.db-journal
|
||||||
|
|
||||||
# Configuration (contains secrets)
|
# Configuration (contains secrets)
|
||||||
config.json
|
**/config.json
|
||||||
|
|
||||||
# Certificates
|
# Certificates
|
||||||
certs/
|
**/certs/
|
||||||
*.crt
|
**/*.crt
|
||||||
*.key
|
**/*.key
|
||||||
*.pem
|
**/*.pem
|
||||||
*.csr
|
**/*.csr
|
||||||
|
|
||||||
|
# Runtime data / uploads / plugins (mounted as volumes at runtime)
|
||||||
|
Overlord-Server/data/
|
||||||
|
Overlord-Server/uploads/
|
||||||
|
Overlord-Server/plugins/
|
||||||
|
plugins/
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -30,16 +36,19 @@ Thumbs.db
|
|||||||
.gitignore
|
.gitignore
|
||||||
.gitattributes
|
.gitattributes
|
||||||
|
|
||||||
# Build outputs
|
# Build outputs (dist-clients is provided by the builder stage in the runtime image)
|
||||||
dist-clients/
|
**/dist-clients/
|
||||||
Overlord-Client/build/
|
Overlord-Client/build/
|
||||||
release/
|
release/
|
||||||
|
|
||||||
|
# Local docker buildx cache (host-only)
|
||||||
|
.docker-cache/
|
||||||
|
|
||||||
**/*.tsbuildinfo
|
**/*.tsbuildinfo
|
||||||
coverage/
|
**/coverage/
|
||||||
tmp/
|
**/tmp/
|
||||||
temp/
|
**/temp/
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
**/*.log
|
||||||
logs/
|
**/logs/
|
||||||
|
|||||||
@@ -60,7 +60,10 @@ plugins/build-netscan.sh
|
|||||||
plugins/build-sysprofile.bat
|
plugins/build-sysprofile.bat
|
||||||
plugins/build-sysprofile.sh
|
plugins/build-sysprofile.sh
|
||||||
plugins/build-ranreg.bat
|
plugins/build-ranreg.bat
|
||||||
|
plugins/build-toxi.bat
|
||||||
Overlord-Server/plugins/*
|
Overlord-Server/plugins/*
|
||||||
|
plugins/toxi/*
|
||||||
|
plugins/toxi copy/*
|
||||||
|
|
||||||
# Runtime data
|
# Runtime data
|
||||||
Overlord-Server/data/
|
Overlord-Server/data/
|
||||||
|
|||||||
+87
-79
@@ -1,96 +1,69 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
# syntax=docker/dockerfile:1.7
|
||||||
# Overlord Server Dockerfile
|
# Overlord Server Dockerfile (multi-stage)
|
||||||
FROM oven/bun:1 AS base
|
#
|
||||||
|
# Stage 1 (builder): full apt toolchain to compile assets + HVNC DLLs.
|
||||||
|
# Stage 2 (runtime, slim): only what the server needs at startup. Cross-compile
|
||||||
|
# toolchains (mingw, aarch64/armv7/musl, Android NDK, ldid, UPX) are downloaded
|
||||||
|
# on first agent build by Overlord-Server/src/server/toolchain-manager.ts and
|
||||||
|
# cached in the persistent /app/data volume.
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Stage 1: builder
|
||||||
|
# ============================================================
|
||||||
|
FROM oven/bun:1 AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install Go for agent building and other tools
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
RUN apt-get update \
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
&& apt-get install -y --no-install-recommends \
|
apt-get update && apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
gcc-mingw-w64-x86-64 \
|
gcc-mingw-w64-x86-64 \
|
||||||
g++-mingw-w64-x86-64 \
|
g++-mingw-w64-x86-64 \
|
||||||
gcc-mingw-w64-i686 \
|
gcc-mingw-w64-i686 \
|
||||||
musl-tools \
|
ca-certificates \
|
||||||
gcc-aarch64-linux-gnu \
|
wget \
|
||||||
gcc-arm-linux-gnueabihf \
|
curl \
|
||||||
openssl \
|
git \
|
||||||
curl \
|
unzip \
|
||||||
ca-certificates \
|
zip
|
||||||
wget \
|
|
||||||
git \
|
|
||||||
unzip \
|
|
||||||
upx-ucl \
|
|
||||||
zip \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install Go (latest stable version)
|
|
||||||
ENV GO_VERSION=1.26.2
|
ENV GO_VERSION=1.26.2
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
RUN case "${TARGETARCH}" in \
|
RUN case "${TARGETARCH:-amd64}" in \
|
||||||
amd64) GO_ARCH=amd64 ;; \
|
amd64) GO_ARCH=amd64 ;; \
|
||||||
arm64) GO_ARCH=arm64 ;; \
|
arm64) GO_ARCH=arm64 ;; \
|
||||||
*) echo "Unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \
|
*) echo "Unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \
|
||||||
esac \
|
esac \
|
||||||
&& wget -q https://go.dev/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz \
|
&& wget -q "https://go.dev/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz" \
|
||||||
&& tar -C /usr/local -xzf go${GO_VERSION}.linux-${GO_ARCH}.tar.gz \
|
&& tar -C /usr/local -xzf "go${GO_VERSION}.linux-${GO_ARCH}.tar.gz" \
|
||||||
&& rm go${GO_VERSION}.linux-${GO_ARCH}.tar.gz
|
&& rm "go${GO_VERSION}.linux-${GO_ARCH}.tar.gz" \
|
||||||
|
&& rm -rf /usr/local/go/test /usr/local/go/api /usr/local/go/doc /usr/local/go/misc
|
||||||
|
|
||||||
ENV PATH="/usr/local/go/bin:${PATH}"
|
ENV PATH="/usr/local/go/bin:/go/bin:${PATH}"
|
||||||
ENV GOPATH="/go"
|
ENV GOPATH="/go"
|
||||||
ENV PATH="${GOPATH}/bin:${PATH}"
|
|
||||||
ENV GOCACHE=/root/.cache/go-build
|
ENV GOCACHE=/root/.cache/go-build
|
||||||
ENV GOMODCACHE=/go/pkg/mod
|
ENV GOMODCACHE=/go/pkg/mod
|
||||||
|
|
||||||
# Install garble for obfuscated agent builds (requires Go 1.25+)
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
--mount=type=cache,target=/go/pkg/mod \
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
go install mvdan.cc/garble@latest
|
go install mvdan.cc/garble@latest
|
||||||
|
|
||||||
# Install Android NDK for Android cross-compilation
|
# Full bun install (includes devDeps needed for tailwind / vendor / minify steps)
|
||||||
ENV ANDROID_NDK_VERSION=r27c
|
|
||||||
ENV ANDROID_NDK_HOME=/opt/android-ndk
|
|
||||||
RUN case "${TARGETARCH}" in \
|
|
||||||
amd64) NDK_HOST="linux-x86_64" ;; \
|
|
||||||
arm64) NDK_HOST="linux-aarch64" ;; \
|
|
||||||
*) echo "Unsupported architecture for Android NDK: ${TARGETARCH}" >&2; exit 1 ;; \
|
|
||||||
esac \
|
|
||||||
&& wget -q "https://dl.google.com/android/repository/android-ndk-${ANDROID_NDK_VERSION}-linux.zip" \
|
|
||||||
&& unzip -q android-ndk-${ANDROID_NDK_VERSION}-linux.zip \
|
|
||||||
&& mv android-ndk-${ANDROID_NDK_VERSION} ${ANDROID_NDK_HOME} \
|
|
||||||
&& rm android-ndk-${ANDROID_NDK_VERSION}-linux.zip
|
|
||||||
|
|
||||||
RUN case "${TARGETARCH}" in \
|
|
||||||
amd64) LDID_ARCH="x86_64" ;; \
|
|
||||||
arm64) LDID_ARCH="aarch64" ;; \
|
|
||||||
*) LDID_ARCH="" ;; \
|
|
||||||
esac \
|
|
||||||
&& if [ -n "${LDID_ARCH}" ]; then \
|
|
||||||
wget -q "https://github.com/ProcursusTeam/ldid/releases/download/v2.1.5-procursus7/ldid_linux_${LDID_ARCH}" \
|
|
||||||
-O /usr/local/bin/ldid \
|
|
||||||
&& chmod +x /usr/local/bin/ldid; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copy package files and lockfile
|
|
||||||
COPY Overlord-Server/package.json Overlord-Server/bun.lock* ./
|
COPY Overlord-Server/package.json Overlord-Server/bun.lock* ./
|
||||||
|
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
|
||||||
# Install dependencies
|
# Server source (Overlord-Server/dist-clients may carry pre-built MSVC DLLs from CI)
|
||||||
RUN bun install --frozen-lockfile
|
|
||||||
|
|
||||||
# Copy source code and client code (needed for builds)
|
|
||||||
COPY Overlord-Server/ ./
|
COPY Overlord-Server/ ./
|
||||||
COPY Overlord-Client/ ./Overlord-Client/
|
|
||||||
|
|
||||||
# Copy HVNCInjection source and build script for cross-compilation
|
# HVNC sources for the cross-compile fallback (used only if no pre-built MSVC DLL).
|
||||||
COPY HVNCInjection/ ./HVNCInjection/
|
COPY HVNCInjection/ ./HVNCInjection/
|
||||||
COPY build-hvnc-dll.sh ./build-hvnc-dll.sh
|
COPY build-hvnc-dll.sh ./build-hvnc-dll.sh
|
||||||
|
|
||||||
# Copy HVNCCapture source and build script for cross-compilation
|
|
||||||
COPY HVNCCapture/ ./HVNCCapture/
|
COPY HVNCCapture/ ./HVNCCapture/
|
||||||
COPY build-hvnc-capture-dll.sh ./build-hvnc-capture-dll.sh
|
COPY build-hvnc-capture-dll.sh ./build-hvnc-capture-dll.sh
|
||||||
|
|
||||||
# Use MSVC-built HVNCInjection DLL if present (preferred, from CI artifact).
|
RUN mkdir -p dist-clients && \
|
||||||
# Fall back to cross-compiling with mingw only if no pre-built DLL exists.
|
if [ -f dist-clients/HVNCInjection.x64.dll ]; then \
|
||||||
RUN if [ -f dist-clients/HVNCInjection.x64.dll ]; then \
|
|
||||||
echo "Using pre-built MSVC HVNCInjection DLL"; \
|
echo "Using pre-built MSVC HVNCInjection DLL"; \
|
||||||
else \
|
else \
|
||||||
chmod +x build-hvnc-dll.sh && \
|
chmod +x build-hvnc-dll.sh && \
|
||||||
@@ -98,8 +71,6 @@ RUN if [ -f dist-clients/HVNCInjection.x64.dll ]; then \
|
|||||||
echo "WARNING: HVNCInjection DLL not available (build with MSVC on Windows)"; \
|
echo "WARNING: HVNCInjection DLL not available (build with MSVC on Windows)"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Use MSVC-built HVNCCapture DLL if present (preferred, from CI artifact).
|
|
||||||
# Fall back to cross-compiling with mingw (C++) only if no pre-built DLL exists.
|
|
||||||
RUN if [ -f dist-clients/HVNCCapture.x64.dll ]; then \
|
RUN if [ -f dist-clients/HVNCCapture.x64.dll ]; then \
|
||||||
echo "Using pre-built MSVC HVNCCapture DLL"; \
|
echo "Using pre-built MSVC HVNCCapture DLL"; \
|
||||||
else \
|
else \
|
||||||
@@ -108,23 +79,58 @@ RUN if [ -f dist-clients/HVNCCapture.x64.dll ]; then \
|
|||||||
echo "WARNING: HVNCCapture DLL not available (build with MSVC on Windows)"; \
|
echo "WARNING: HVNCCapture DLL not available (build with MSVC on Windows)"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create necessary directories
|
# Tailwind CSS + vendored frontend assets
|
||||||
RUN mkdir -p certs public data
|
|
||||||
|
|
||||||
# Build Tailwind CSS and vendor assets
|
|
||||||
RUN bun run build:css && bun run vendor \
|
RUN bun run build:css && bun run vendor \
|
||||||
&& test -s ./public/assets/tailwind.css && test -d ./public/vendor/fontawesome
|
&& test -s ./public/assets/tailwind.css && test -d ./public/vendor/fontawesome
|
||||||
|
|
||||||
# NOTE: We intentionally do NOT use `bun build --compile` here.
|
|
||||||
# The compiled standalone binary runs from a virtual bunfs filesystem that
|
|
||||||
# cannot reliably load native modules like `sharp` from /app/node_modules,
|
|
||||||
# even when marked --external. Running the TS entrypoint directly with bun
|
|
||||||
# keeps normal Node-style module resolution and lets sharp load correctly.
|
|
||||||
|
|
||||||
# Expose the default port
|
# ============================================================
|
||||||
|
# Stage 2: runtime (slim)
|
||||||
|
# ============================================================
|
||||||
|
FROM oven/bun:1-slim AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# openssl/ca-certificates: TLS cert generation + HTTPS validation.
|
||||||
|
# wget/tar/unzip/xz-utils: required by toolchain-manager for on-demand downloads.
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
|
apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
openssl \
|
||||||
|
ca-certificates \
|
||||||
|
wget \
|
||||||
|
tar \
|
||||||
|
unzip \
|
||||||
|
xz-utils \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Reuse Go + garble from the builder so we don't re-download.
|
||||||
|
COPY --from=builder /usr/local/go /usr/local/go
|
||||||
|
COPY --from=builder /go/bin/garble /go/bin/garble
|
||||||
|
|
||||||
|
ENV PATH="/usr/local/go/bin:/go/bin:${PATH}"
|
||||||
|
ENV GOPATH="/go"
|
||||||
|
ENV GOCACHE=/root/.cache/go-build
|
||||||
|
ENV GOMODCACHE=/go/pkg/mod
|
||||||
|
|
||||||
|
# Production-only node_modules (drops tailwind, terser, postcss, typescript, ...).
|
||||||
|
COPY Overlord-Server/package.json Overlord-Server/bun.lock* ./
|
||||||
|
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
||||||
|
bun install --production --frozen-lockfile
|
||||||
|
|
||||||
|
# Server source.
|
||||||
|
COPY Overlord-Server/ ./
|
||||||
|
|
||||||
|
# Built artifacts from the builder stage (overwrites anything from the source copy).
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/dist-clients ./dist-clients
|
||||||
|
|
||||||
|
# Go agent source needed at every agent build.
|
||||||
|
COPY Overlord-Client/ ./Overlord-Client/
|
||||||
|
|
||||||
|
RUN mkdir -p certs data
|
||||||
|
|
||||||
EXPOSE 5173
|
EXPOSE 5173
|
||||||
|
|
||||||
# Set environment variables (can be overridden)
|
|
||||||
ENV PORT=5173
|
ENV PORT=5173
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
ENV DATA_DIR=/app/data
|
ENV DATA_DIR=/app/data
|
||||||
@@ -132,5 +138,7 @@ ENV NODE_ENV=production
|
|||||||
ENV OVERLORD_ROOT=/app
|
ENV OVERLORD_ROOT=/app
|
||||||
ENV NODE_PATH=/app/node_modules
|
ENV NODE_PATH=/app/node_modules
|
||||||
|
|
||||||
# Run the server directly with bun (no standalone-binary compile step)
|
# NOTE: We intentionally do NOT use `bun build --compile` here.
|
||||||
|
# The compiled standalone binary runs from a virtual bunfs filesystem that
|
||||||
|
# cannot reliably load native modules like `sharp` from /app/node_modules.
|
||||||
CMD ["bun", "run", "src/index.ts"]
|
CMD ["bun", "run", "src/index.ts"]
|
||||||
|
|||||||
@@ -15,22 +15,12 @@
|
|||||||
"minify": "bun run scripts/minify-public.ts"
|
"minify": "bun run scripts/minify-public.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^5.2.8",
|
|
||||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
|
||||||
"@fortawesome/fontawesome-free": "6.5.2",
|
|
||||||
"@msgpack/msgpack": "^3.0.0",
|
"@msgpack/msgpack": "^3.0.0",
|
||||||
"@solana/web3.js": "^1.98.4",
|
"@solana/web3.js": "^1.98.4",
|
||||||
"ace-builds": "1.36.5",
|
|
||||||
"adm-zip": "^0.5.12",
|
"adm-zip": "^0.5.12",
|
||||||
"animejs": "3.2.2",
|
|
||||||
"ansi-to-html": "0.7.2",
|
"ansi-to-html": "0.7.2",
|
||||||
"chart.js": "4.4.0",
|
|
||||||
"codemirror": "5.65.16",
|
|
||||||
"flag-icons": "latest",
|
|
||||||
"geoip-lite": "latest",
|
"geoip-lite": "latest",
|
||||||
"highlight.js": "11.9.0",
|
|
||||||
"jose": "^6.1.3",
|
"jose": "^6.1.3",
|
||||||
"leaflet": "1.9.4",
|
|
||||||
"msgpackr": "1.11.8",
|
"msgpackr": "1.11.8",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
@@ -38,13 +28,23 @@
|
|||||||
"winston": "^3.11.0"
|
"winston": "^3.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@fontsource/inter": "^5.2.8",
|
||||||
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||||
|
"@fortawesome/fontawesome-free": "6.5.2",
|
||||||
"@tailwindcss/cli": "^4.1.18",
|
"@tailwindcss/cli": "^4.1.18",
|
||||||
"@types/bun": "^1.3.9",
|
"@types/bun": "^1.3.9",
|
||||||
"@types/geoip-lite": "latest",
|
"@types/geoip-lite": "latest",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
|
"ace-builds": "1.36.5",
|
||||||
|
"animejs": "3.2.2",
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
|
"chart.js": "4.4.0",
|
||||||
"clean-css": "^5.3.3",
|
"clean-css": "^5.3.3",
|
||||||
|
"codemirror": "5.65.16",
|
||||||
|
"flag-icons": "latest",
|
||||||
|
"highlight.js": "11.9.0",
|
||||||
"html-minifier-terser": "^7.2.0",
|
"html-minifier-terser": "^7.2.0",
|
||||||
|
"leaflet": "1.9.4",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"terser": "^5.46.1",
|
"terser": "^5.46.1",
|
||||||
|
|||||||
@@ -434,8 +434,7 @@ async function loadPluginsForClient(clientId) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const btn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
btn.className =
|
btn.className = "ctx-item ctx-plugin-item";
|
||||||
"w-full text-left px-3 py-2 rounded-lg border border-slate-700 bg-slate-800/60 hover:bg-slate-700 text-slate-100 flex items-center gap-2 justify-between";
|
|
||||||
btn.dataset.plugin = plugin.id;
|
btn.dataset.plugin = plugin.id;
|
||||||
btn.dataset.loaded = plugin.loaded ? "true" : "false";
|
btn.dataset.loaded = plugin.loaded ? "true" : "false";
|
||||||
if (plugin.lastError) {
|
if (plugin.lastError) {
|
||||||
@@ -445,55 +444,54 @@ async function loadPluginsForClient(clientId) {
|
|||||||
const sig = plugin.signature;
|
const sig = plugin.signature;
|
||||||
if (sig && sig.signed && !sig.valid) {
|
if (sig && sig.signed && !sig.valid) {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.classList.add("opacity-50", "cursor-not-allowed");
|
btn.setAttribute("aria-disabled", "true");
|
||||||
btn.title = "Plugin signature is invalid — cannot load";
|
btn.title = "Plugin signature is invalid — cannot load";
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = document.createElement("span");
|
|
||||||
label.className = "flex items-center gap-1";
|
|
||||||
const labelIcon = document.createElement("i");
|
const labelIcon = document.createElement("i");
|
||||||
labelIcon.className = "fa-solid fa-puzzle-piece";
|
labelIcon.className =
|
||||||
label.appendChild(labelIcon);
|
"fa-solid fa-puzzle-piece ctx-icon " +
|
||||||
label.append(` ${plugin.name || plugin.id}`);
|
(plugin.loaded ? "text-emerald-400" : "text-fuchsia-400");
|
||||||
|
btn.appendChild(labelIcon);
|
||||||
|
|
||||||
|
const label = document.createElement("span");
|
||||||
|
label.className = "ctx-plugin-label";
|
||||||
|
label.textContent = plugin.name || plugin.id;
|
||||||
|
btn.appendChild(label);
|
||||||
|
|
||||||
if (sig) {
|
if (sig) {
|
||||||
const trustIcon = document.createElement("i");
|
const trustIcon = document.createElement("i");
|
||||||
if (sig.signed && !sig.valid) {
|
if (sig.signed && !sig.valid) {
|
||||||
trustIcon.className = "fa-solid fa-shield-xmark text-red-400 text-xs ml-1";
|
trustIcon.className = "fa-solid fa-shield-xmark ctx-plugin-trust text-red-400";
|
||||||
trustIcon.title = "Invalid signature";
|
trustIcon.title = "Invalid signature";
|
||||||
} else if (sig.signed && sig.valid && sig.trusted) {
|
} else if (sig.signed && sig.valid && sig.trusted) {
|
||||||
trustIcon.className = "fa-solid fa-shield-check text-emerald-400 text-xs ml-1";
|
trustIcon.className = "fa-solid fa-shield-check ctx-plugin-trust text-emerald-400";
|
||||||
trustIcon.title = "Trusted";
|
trustIcon.title = "Trusted";
|
||||||
} else if (sig.signed && sig.valid && !sig.trusted) {
|
} else if (sig.signed && sig.valid && !sig.trusted) {
|
||||||
trustIcon.className = "fa-solid fa-shield text-yellow-400 text-xs ml-1";
|
trustIcon.className = "fa-solid fa-shield ctx-plugin-trust text-yellow-400";
|
||||||
trustIcon.title = "Signed but untrusted";
|
trustIcon.title = "Signed but untrusted";
|
||||||
} else {
|
} else {
|
||||||
trustIcon.className = "fa-solid fa-shield-halved text-orange-400 text-xs ml-1";
|
trustIcon.className = "fa-solid fa-shield-halved ctx-plugin-trust text-orange-400";
|
||||||
trustIcon.title = "Unsigned";
|
trustIcon.title = "Unsigned";
|
||||||
}
|
}
|
||||||
label.appendChild(trustIcon);
|
btn.appendChild(trustIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
const badge = document.createElement("span");
|
const badge = document.createElement("span");
|
||||||
badge.className =
|
badge.className = "ctx-plugin-badge" + (plugin.loaded ? " is-loaded" : "");
|
||||||
"text-[10px] uppercase tracking-wider px-2 py-0.5 rounded-full border" +
|
|
||||||
(plugin.loaded
|
|
||||||
? " border-emerald-600 text-emerald-300 bg-emerald-900/40"
|
|
||||||
: " border-slate-600 text-slate-300 bg-slate-800/60");
|
|
||||||
badge.textContent = plugin.loaded ? "loaded" : "available";
|
badge.textContent = plugin.loaded ? "loaded" : "available";
|
||||||
btn.appendChild(label);
|
|
||||||
btn.appendChild(badge);
|
btn.appendChild(badge);
|
||||||
container.appendChild(btn);
|
container.appendChild(btn);
|
||||||
|
|
||||||
if (plugin.loaded) {
|
if (plugin.loaded) {
|
||||||
const unloadBtn = document.createElement("button");
|
const unloadBtn = document.createElement("button");
|
||||||
unloadBtn.className =
|
unloadBtn.className = "ctx-item";
|
||||||
"w-full text-left px-3 py-2 rounded-lg border border-red-800 bg-red-900/30 hover:bg-red-800/60 text-red-100 flex items-center gap-2";
|
|
||||||
unloadBtn.dataset.pluginUnload = plugin.id;
|
unloadBtn.dataset.pluginUnload = plugin.id;
|
||||||
const unloadIcon = document.createElement("i");
|
const unloadIcon = document.createElement("i");
|
||||||
unloadIcon.className = "fa-solid fa-plug-circle-xmark";
|
unloadIcon.className = "fa-solid fa-plug-circle-xmark ctx-icon text-red-400";
|
||||||
|
unloadBtn.appendChild(unloadIcon);
|
||||||
const unloadText = document.createElement("span");
|
const unloadText = document.createElement("span");
|
||||||
unloadText.textContent = `Unload ${plugin.name || plugin.id}`;
|
unloadText.textContent = `Unload ${plugin.name || plugin.id}`;
|
||||||
unloadBtn.appendChild(unloadIcon);
|
|
||||||
unloadBtn.appendChild(unloadText);
|
unloadBtn.appendChild(unloadText);
|
||||||
container.appendChild(unloadBtn);
|
container.appendChild(unloadBtn);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,33 @@ menuStyle.textContent = `
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
.ctx-plugin-item {
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
.ctx-plugin-label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.ctx-plugin-trust {
|
||||||
|
font-size: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.ctx-plugin-badge {
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(148,163,184,0.25);
|
||||||
|
background: rgba(15,23,42,0.6);
|
||||||
|
color: #94a3b8;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.ctx-plugin-badge.is-loaded {
|
||||||
|
border-color: rgba(16,185,129,0.45);
|
||||||
|
background: rgba(6,78,59,0.35);
|
||||||
|
color: #6ee7b7;
|
||||||
|
}
|
||||||
.ctx-chevron {
|
.ctx-chevron {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import * as buildManager from "../build/buildManager";
|
|||||||
import type { BuildStream } from "../build/types";
|
import type { BuildStream } from "../build/types";
|
||||||
import { ALLOWED_PLATFORMS } from "./validation-constants";
|
import { ALLOWED_PLATFORMS } from "./validation-constants";
|
||||||
import { resolveRuntimeRoot } from "./runtime-paths";
|
import { resolveRuntimeRoot } from "./runtime-paths";
|
||||||
|
import {
|
||||||
|
ensureToolchain,
|
||||||
|
toolchainKeyForTarget,
|
||||||
|
type EnsuredToolchain,
|
||||||
|
} from "./toolchain-manager";
|
||||||
|
|
||||||
function isClientModuleDir(dir: string): boolean {
|
function isClientModuleDir(dir: string): boolean {
|
||||||
return (
|
return (
|
||||||
@@ -45,12 +50,11 @@ function resolveClientBuildCacheRoot(): string {
|
|||||||
return path.resolve(ensureDataDir(), "client-build-cache");
|
return path.resolve(ensureDataDir(), "client-build-cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAndroidNdkToolchainBin(): string | null {
|
function resolveExplicitAndroidNdkToolchainBin(): string | null {
|
||||||
const explicit = process.env.ANDROID_NDK_HOME?.trim();
|
const explicit = process.env.ANDROID_NDK_HOME?.trim();
|
||||||
const ndkHome = explicit || (process.platform === "win32" ? "" : "/opt/android-ndk");
|
if (!explicit) return null;
|
||||||
if (!ndkHome) return null;
|
|
||||||
const hostArch = process.arch === "arm64" ? "linux-aarch64" : "linux-x86_64";
|
const hostArch = process.arch === "arm64" ? "linux-aarch64" : "linux-x86_64";
|
||||||
const toolchainBin = path.join(ndkHome, "toolchains", "llvm", "prebuilt", hostArch, "bin");
|
const toolchainBin = path.join(explicit, "toolchains", "llvm", "prebuilt", hostArch, "bin");
|
||||||
try {
|
try {
|
||||||
return fs.existsSync(toolchainBin) ? toolchainBin : null;
|
return fs.existsSync(toolchainBin) ? toolchainBin : null;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -297,13 +301,21 @@ export async function startBuildProcess(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ndkBin = hasAndroidTargets ? resolveAndroidNdkToolchainBin() : null;
|
let ndkBin: string | null = null;
|
||||||
if (hasAndroidTargets && !ndkBin) {
|
if (hasAndroidTargets) {
|
||||||
sendToStream({
|
ndkBin = resolveExplicitAndroidNdkToolchainBin();
|
||||||
type: "output",
|
if (!ndkBin) {
|
||||||
text: "Warning: Android NDK not found. Android builds require the NDK. Install it to /opt/android-ndk or set the ANDROID_NDK_HOME environment variable.\n",
|
try {
|
||||||
level: "warn",
|
const ndk = await ensureToolchain("android-ndk", sendToStream);
|
||||||
});
|
ndkBin = ndk.binDir;
|
||||||
|
} catch (err: any) {
|
||||||
|
sendToStream({
|
||||||
|
type: "output",
|
||||||
|
text: `Warning: Android NDK download failed (${err.message || err}). Android builds require the NDK. Set ANDROID_NDK_HOME to use a pre-installed NDK.\n`,
|
||||||
|
level: "warn",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let buildMutex = "";
|
let buildMutex = "";
|
||||||
@@ -323,16 +335,21 @@ export async function startBuildProcess(
|
|||||||
|
|
||||||
let upxBin: string | null = null;
|
let upxBin: string | null = null;
|
||||||
if (config.enableUpx) {
|
if (config.enableUpx) {
|
||||||
const upxFound = await checkUpxAvailable(sendToStream);
|
if (await checkUpxAvailable(sendToStream)) {
|
||||||
if (!upxFound) {
|
upxBin = "upx";
|
||||||
sendToStream({
|
} else {
|
||||||
type: "output",
|
try {
|
||||||
text: "ERROR: UPX is not installed or not found in PATH. Please install UPX (https://upx.github.io) and ensure it is available on PATH, then retry.\n",
|
const upxTool = await ensureToolchain("upx", sendToStream);
|
||||||
level: "error",
|
upxBin = path.join(upxTool.binDir, "upx");
|
||||||
});
|
} catch (err: any) {
|
||||||
throw new Error("UPX not found");
|
sendToStream({
|
||||||
|
type: "output",
|
||||||
|
text: `ERROR: UPX is not installed and on-demand download failed (${err.message || err}).\n`,
|
||||||
|
level: "error",
|
||||||
|
});
|
||||||
|
throw new Error("UPX not found");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
upxBin = "upx";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAssemblyData = !!(config.assemblyTitle || config.assemblyProduct || config.assemblyCompany || config.assemblyVersion || config.assemblyCopyright || config.iconBase64 || config.requireAdmin);
|
const hasAssemblyData = !!(config.assemblyTitle || config.assemblyProduct || config.assemblyCompany || config.assemblyVersion || config.assemblyCopyright || config.iconBase64 || config.requireAdmin);
|
||||||
@@ -668,33 +685,43 @@ func runBoundFiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (env.CGO_ENABLED === "1") {
|
if (env.CGO_ENABLED === "1") {
|
||||||
const cCompilerByTarget: Record<string, string> = {
|
let cc: string | undefined;
|
||||||
"linux/amd64": "musl-gcc",
|
let cxx: string | undefined;
|
||||||
"linux/arm64": "aarch64-linux-gnu-gcc",
|
let extraBinDir: string | undefined;
|
||||||
"linux/arm/v7": "arm-linux-gnueabihf-gcc",
|
|
||||||
"windows/amd64": "x86_64-w64-mingw32-gcc",
|
if (os === "android" && ndkBin) {
|
||||||
"windows/386": "i686-w64-mingw32-gcc",
|
const ndkCcByTarget: Record<string, string> = {
|
||||||
...(ndkBin ? {
|
"android/amd64": "x86_64-linux-android21-clang",
|
||||||
"android/amd64": path.join(ndkBin, "x86_64-linux-android21-clang"),
|
"android/arm64": "aarch64-linux-android21-clang",
|
||||||
"android/arm64": path.join(ndkBin, "aarch64-linux-android21-clang"),
|
"android/arm/v7": "armv7a-linux-androideabi21-clang",
|
||||||
"android/arm/v7": path.join(ndkBin, "armv7a-linux-androideabi21-clang"),
|
};
|
||||||
} : {}),
|
const basename = ndkCcByTarget[targetKey];
|
||||||
};
|
if (basename) {
|
||||||
const cxxCompilerByTarget: Record<string, string> = {
|
cc = path.join(ndkBin, basename);
|
||||||
"linux/amd64": "g++",
|
cxx = path.join(ndkBin, `${basename}++`);
|
||||||
"linux/arm64": "aarch64-linux-gnu-g++",
|
extraBinDir = ndkBin;
|
||||||
"linux/arm/v7": "arm-linux-gnueabihf-g++",
|
env.AR = path.join(ndkBin, "llvm-ar");
|
||||||
"windows/amd64": "x86_64-w64-mingw32-g++",
|
}
|
||||||
"windows/386": "i686-w64-mingw32-g++",
|
} else {
|
||||||
...(ndkBin ? {
|
const tcKey = toolchainKeyForTarget(targetKey);
|
||||||
"android/amd64": path.join(ndkBin, "x86_64-linux-android21-clang++"),
|
if (tcKey) {
|
||||||
"android/arm64": path.join(ndkBin, "aarch64-linux-android21-clang++"),
|
try {
|
||||||
"android/arm/v7": path.join(ndkBin, "armv7a-linux-androideabi21-clang++"),
|
const tc: EnsuredToolchain = await ensureToolchain(tcKey, sendToStream);
|
||||||
} : {}),
|
cc = tc.ccPath;
|
||||||
};
|
cxx = tc.cxxPath;
|
||||||
|
extraBinDir = tc.binDir;
|
||||||
|
if (tc.arPath) env.AR = tc.arPath;
|
||||||
|
} catch (err: any) {
|
||||||
|
sendToStream({
|
||||||
|
type: "output",
|
||||||
|
text: `ERROR: Failed to provision cross-compile toolchain for ${targetKey}: ${err.message || err}\n`,
|
||||||
|
level: "error",
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const cc = cCompilerByTarget[targetKey];
|
|
||||||
const cxx = cxxCompilerByTarget[targetKey];
|
|
||||||
if (cc) {
|
if (cc) {
|
||||||
env.CC = cc;
|
env.CC = cc;
|
||||||
sendToStream({ type: "output", text: `CGO compiler: ${cc}\n`, level: "info" });
|
sendToStream({ type: "output", text: `CGO compiler: ${cc}\n`, level: "info" });
|
||||||
@@ -708,8 +735,9 @@ func runBoundFiles() {
|
|||||||
if (cxx) {
|
if (cxx) {
|
||||||
env.CXX = cxx;
|
env.CXX = cxx;
|
||||||
}
|
}
|
||||||
if (os === "android" && ndkBin) {
|
if (extraBinDir) {
|
||||||
env.AR = path.join(ndkBin, "llvm-ar");
|
const sep = process.platform === "win32" ? ";" : ":";
|
||||||
|
env.PATH = `${extraBinDir}${sep}${env.PATH || process.env.PATH || ""}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1017,18 +1045,31 @@ func runBoundFiles() {
|
|||||||
</plist>`;
|
</plist>`;
|
||||||
fs.writeFileSync(path.join(payloadAppDir, "Info.plist"), infoPlist, "utf8");
|
fs.writeFileSync(path.join(payloadAppDir, "Info.plist"), infoPlist, "utf8");
|
||||||
|
|
||||||
// Attempt pseudo-signing with ldid if available
|
// Attempt pseudo-signing with ldid (system-installed or downloaded on demand).
|
||||||
try {
|
try {
|
||||||
const ldidResult = await $`which ldid`.nothrow().quiet();
|
let ldidBin: string | null = null;
|
||||||
if (ldidResult.exitCode === 0) {
|
const sysLdid = await $`which ldid`.nothrow().quiet();
|
||||||
const ldidSign = await $`ldid -S ${path.join(payloadAppDir, appName)}`.nothrow().quiet();
|
if (sysLdid.exitCode === 0) {
|
||||||
|
ldidBin = "ldid";
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const tc = await ensureToolchain("ldid", sendToStream);
|
||||||
|
ldidBin = path.join(tc.binDir, "ldid");
|
||||||
|
} catch (dlErr: any) {
|
||||||
|
sendToStream({
|
||||||
|
type: "output",
|
||||||
|
text: `ldid not found and download failed (${dlErr.message || dlErr}); skipping pseudo-signing.\n`,
|
||||||
|
level: "warn",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ldidBin) {
|
||||||
|
const ldidSign = await $`${ldidBin} -S ${path.join(payloadAppDir, appName)}`.nothrow().quiet();
|
||||||
if (ldidSign.exitCode === 0) {
|
if (ldidSign.exitCode === 0) {
|
||||||
sendToStream({ type: "output", text: `Pseudo-signed with ldid\n`, level: "info" });
|
sendToStream({ type: "output", text: `Pseudo-signed with ldid\n`, level: "info" });
|
||||||
} else {
|
} else {
|
||||||
sendToStream({ type: "output", text: `WARNING: ldid signing failed (non-fatal)\n`, level: "warn" });
|
sendToStream({ type: "output", text: `WARNING: ldid signing failed (non-fatal)\n`, level: "warn" });
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
sendToStream({ type: "output", text: `ldid not found; skipping pseudo-signing (install ldid for TrollStore/sideload compatibility)\n`, level: "warn" });
|
|
||||||
}
|
}
|
||||||
} catch { /* ldid is optional */ }
|
} catch { /* ldid is optional */ }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import { $ } from "bun";
|
||||||
|
import { ensureDataDir } from "../paths";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
type SendToStream = (data: any) => void;
|
||||||
|
|
||||||
|
export type ToolchainKey =
|
||||||
|
| "mingw-w64-x64"
|
||||||
|
| "mingw-w64-x86"
|
||||||
|
| "linux-musl-x64"
|
||||||
|
| "linux-musl-arm64"
|
||||||
|
| "linux-musl-armv7"
|
||||||
|
| "android-ndk"
|
||||||
|
| "ldid"
|
||||||
|
| "upx";
|
||||||
|
|
||||||
|
export type EnsuredToolchain = {
|
||||||
|
key: ToolchainKey;
|
||||||
|
binDir: string;
|
||||||
|
ccPath?: string;
|
||||||
|
cxxPath?: string;
|
||||||
|
arPath?: string;
|
||||||
|
rootDir: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ArchiveFormat = "tar.gz" | "tar.xz" | "zip" | "binary";
|
||||||
|
|
||||||
|
type Manifest = {
|
||||||
|
displayName: string;
|
||||||
|
url: string;
|
||||||
|
approxSizeMB: number;
|
||||||
|
archive: ArchiveFormat;
|
||||||
|
binSubdir: string;
|
||||||
|
ccBasename?: string;
|
||||||
|
cxxBasename?: string;
|
||||||
|
arBasename?: string;
|
||||||
|
binaryFilename?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ANDROID_NDK_VERSION = "r27c";
|
||||||
|
|
||||||
|
function ndkHostTriple(): string {
|
||||||
|
return process.arch === "arm64" ? "linux-aarch64" : "linux-x86_64";
|
||||||
|
}
|
||||||
|
|
||||||
|
function ldidArch(): string {
|
||||||
|
return process.arch === "arm64" ? "aarch64" : "x86_64";
|
||||||
|
}
|
||||||
|
|
||||||
|
const MANIFESTS: Record<ToolchainKey, Manifest> = {
|
||||||
|
"mingw-w64-x64": {
|
||||||
|
displayName: "mingw-w64 x86_64 cross-compiler",
|
||||||
|
url: "https://musl.cc/x86_64-w64-mingw32-cross.tgz",
|
||||||
|
approxSizeMB: 70,
|
||||||
|
archive: "tar.gz",
|
||||||
|
binSubdir: "x86_64-w64-mingw32-cross/bin",
|
||||||
|
ccBasename: "x86_64-w64-mingw32-gcc",
|
||||||
|
cxxBasename: "x86_64-w64-mingw32-g++",
|
||||||
|
arBasename: "x86_64-w64-mingw32-ar",
|
||||||
|
},
|
||||||
|
"mingw-w64-x86": {
|
||||||
|
displayName: "mingw-w64 i686 cross-compiler",
|
||||||
|
url: "https://musl.cc/i686-w64-mingw32-cross.tgz",
|
||||||
|
approxSizeMB: 65,
|
||||||
|
archive: "tar.gz",
|
||||||
|
binSubdir: "i686-w64-mingw32-cross/bin",
|
||||||
|
ccBasename: "i686-w64-mingw32-gcc",
|
||||||
|
cxxBasename: "i686-w64-mingw32-g++",
|
||||||
|
arBasename: "i686-w64-mingw32-ar",
|
||||||
|
},
|
||||||
|
"linux-musl-x64": {
|
||||||
|
displayName: "musl x86_64 Linux cross-compiler",
|
||||||
|
url: "https://musl.cc/x86_64-linux-musl-cross.tgz",
|
||||||
|
approxSizeMB: 60,
|
||||||
|
archive: "tar.gz",
|
||||||
|
binSubdir: "x86_64-linux-musl-cross/bin",
|
||||||
|
ccBasename: "x86_64-linux-musl-gcc",
|
||||||
|
cxxBasename: "x86_64-linux-musl-g++",
|
||||||
|
arBasename: "x86_64-linux-musl-ar",
|
||||||
|
},
|
||||||
|
"linux-musl-arm64": {
|
||||||
|
displayName: "musl aarch64 Linux cross-compiler",
|
||||||
|
url: "https://musl.cc/aarch64-linux-musl-cross.tgz",
|
||||||
|
approxSizeMB: 60,
|
||||||
|
archive: "tar.gz",
|
||||||
|
binSubdir: "aarch64-linux-musl-cross/bin",
|
||||||
|
ccBasename: "aarch64-linux-musl-gcc",
|
||||||
|
cxxBasename: "aarch64-linux-musl-g++",
|
||||||
|
arBasename: "aarch64-linux-musl-ar",
|
||||||
|
},
|
||||||
|
"linux-musl-armv7": {
|
||||||
|
displayName: "musl armv7l Linux cross-compiler",
|
||||||
|
url: "https://musl.cc/armv7l-linux-musleabihf-cross.tgz",
|
||||||
|
approxSizeMB: 60,
|
||||||
|
archive: "tar.gz",
|
||||||
|
binSubdir: "armv7l-linux-musleabihf-cross/bin",
|
||||||
|
ccBasename: "armv7l-linux-musleabihf-gcc",
|
||||||
|
cxxBasename: "armv7l-linux-musleabihf-g++",
|
||||||
|
arBasename: "armv7l-linux-musleabihf-ar",
|
||||||
|
},
|
||||||
|
"android-ndk": {
|
||||||
|
displayName: `Android NDK ${ANDROID_NDK_VERSION}`,
|
||||||
|
url: `https://dl.google.com/android/repository/android-ndk-${ANDROID_NDK_VERSION}-linux.zip`,
|
||||||
|
approxSizeMB: 1500,
|
||||||
|
archive: "zip",
|
||||||
|
binSubdir: `android-ndk-${ANDROID_NDK_VERSION}/toolchains/llvm/prebuilt/${ndkHostTriple()}/bin`,
|
||||||
|
arBasename: "llvm-ar",
|
||||||
|
},
|
||||||
|
ldid: {
|
||||||
|
displayName: "ldid (iOS pseudo-signing)",
|
||||||
|
url: `https://github.com/ProcursusTeam/ldid/releases/download/v2.1.5-procursus7/ldid_linux_${ldidArch()}`,
|
||||||
|
approxSizeMB: 2,
|
||||||
|
archive: "binary",
|
||||||
|
binSubdir: "bin",
|
||||||
|
binaryFilename: "ldid",
|
||||||
|
},
|
||||||
|
upx: {
|
||||||
|
displayName: "UPX 4.2.4",
|
||||||
|
url: "https://github.com/upx/upx/releases/download/v4.2.4/upx-4.2.4-amd64_linux.tar.xz",
|
||||||
|
approxSizeMB: 1,
|
||||||
|
archive: "tar.xz",
|
||||||
|
binSubdir: "upx-4.2.4-amd64_linux",
|
||||||
|
binaryFilename: "upx",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const inFlight = new Map<ToolchainKey, Promise<EnsuredToolchain>>();
|
||||||
|
|
||||||
|
export function getToolchainRoot(): string {
|
||||||
|
const explicit = process.env.OVERLORD_TOOLCHAIN_DIR?.trim();
|
||||||
|
if (explicit) return path.resolve(explicit);
|
||||||
|
return path.join(ensureDataDir(), "toolchains");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolchainDir(key: ToolchainKey): string {
|
||||||
|
return path.join(getToolchainRoot(), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sentinelPath(key: ToolchainKey): string {
|
||||||
|
return path.join(toolchainDir(key), ".installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInstalled(key: ToolchainKey): boolean {
|
||||||
|
try {
|
||||||
|
return fs.existsSync(sentinelPath(key));
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveEnsured(key: ToolchainKey): EnsuredToolchain {
|
||||||
|
const m = MANIFESTS[key];
|
||||||
|
const rootDir = toolchainDir(key);
|
||||||
|
const binDir = path.join(rootDir, m.binSubdir);
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
rootDir,
|
||||||
|
binDir,
|
||||||
|
ccPath: m.ccBasename ? path.join(binDir, m.ccBasename) : undefined,
|
||||||
|
cxxPath: m.cxxBasename ? path.join(binDir, m.cxxBasename) : undefined,
|
||||||
|
arPath: m.arBasename ? path.join(binDir, m.arBasename) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadAndExtract(
|
||||||
|
key: ToolchainKey,
|
||||||
|
send: SendToStream,
|
||||||
|
): Promise<void> {
|
||||||
|
const m = MANIFESTS[key];
|
||||||
|
const rootDir = toolchainDir(key);
|
||||||
|
const tmpDir = `${rootDir}.tmp-${process.pid}-${Date.now()}`;
|
||||||
|
|
||||||
|
fs.mkdirSync(getToolchainRoot(), { recursive: true });
|
||||||
|
fs.mkdirSync(tmpDir, { recursive: true });
|
||||||
|
|
||||||
|
const archiveName =
|
||||||
|
m.archive === "binary"
|
||||||
|
? m.binaryFilename || "download"
|
||||||
|
: path.basename(new URL(m.url).pathname);
|
||||||
|
const archivePath = path.join(tmpDir, archiveName);
|
||||||
|
|
||||||
|
send({
|
||||||
|
type: "output",
|
||||||
|
text: `[toolchain] Downloading ${m.displayName} (~${m.approxSizeMB}MB) from ${m.url}\n`,
|
||||||
|
level: "info",
|
||||||
|
});
|
||||||
|
logger.info(`[toolchain] downloading ${key} from ${m.url}`);
|
||||||
|
|
||||||
|
const dl = await $`wget --tries=2 --timeout=60 -q -O ${archivePath} ${m.url}`
|
||||||
|
.nothrow();
|
||||||
|
if (dl.exitCode !== 0) {
|
||||||
|
const stderr = dl.stderr.toString().trim();
|
||||||
|
throw new Error(
|
||||||
|
`Failed to download ${m.displayName} (wget exit ${dl.exitCode}): ${stderr || "no stderr"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
send({
|
||||||
|
type: "output",
|
||||||
|
text: `[toolchain] Extracting ${m.displayName}...\n`,
|
||||||
|
level: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (m.archive) {
|
||||||
|
case "tar.gz": {
|
||||||
|
const r = await $`tar -xzf ${archivePath} -C ${tmpDir}`.nothrow();
|
||||||
|
if (r.exitCode !== 0) {
|
||||||
|
throw new Error(`tar -xzf failed for ${key}: ${r.stderr.toString()}`);
|
||||||
|
}
|
||||||
|
fs.unlinkSync(archivePath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "tar.xz": {
|
||||||
|
const r = await $`tar -xJf ${archivePath} -C ${tmpDir}`.nothrow();
|
||||||
|
if (r.exitCode !== 0) {
|
||||||
|
throw new Error(`tar -xJf failed for ${key}: ${r.stderr.toString()}`);
|
||||||
|
}
|
||||||
|
fs.unlinkSync(archivePath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "zip": {
|
||||||
|
const r = await $`unzip -q ${archivePath} -d ${tmpDir}`.nothrow();
|
||||||
|
if (r.exitCode !== 0) {
|
||||||
|
throw new Error(`unzip failed for ${key}: ${r.stderr.toString()}`);
|
||||||
|
}
|
||||||
|
fs.unlinkSync(archivePath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "binary": {
|
||||||
|
// Move single-binary download into a bin/ subdir so binSubdir resolves.
|
||||||
|
const binDir = path.join(tmpDir, m.binSubdir);
|
||||||
|
fs.mkdirSync(binDir, { recursive: true });
|
||||||
|
const finalBin = path.join(binDir, m.binaryFilename || "download");
|
||||||
|
fs.renameSync(archivePath, finalBin);
|
||||||
|
fs.chmodSync(finalBin, 0o755);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomic publish: rename tmpDir → rootDir. If another process already
|
||||||
|
// created rootDir (race), fall back to checking the sentinel.
|
||||||
|
try {
|
||||||
|
fs.renameSync(tmpDir, rootDir);
|
||||||
|
} catch (err: any) {
|
||||||
|
// If another worker won the race and rootDir already exists, accept it.
|
||||||
|
if (err.code === "ENOTEMPTY" || err.code === "EEXIST" || err.code === "EPERM") {
|
||||||
|
try {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
if (isInstalled(key)) return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(sentinelPath(key), `${m.url}\n${new Date().toISOString()}\n`);
|
||||||
|
send({
|
||||||
|
type: "output",
|
||||||
|
text: `[toolchain] ${m.displayName} ready at ${rootDir}\n`,
|
||||||
|
level: "info",
|
||||||
|
});
|
||||||
|
logger.info(`[toolchain] installed ${key} at ${rootDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureToolchain(
|
||||||
|
key: ToolchainKey,
|
||||||
|
send: SendToStream,
|
||||||
|
): Promise<EnsuredToolchain> {
|
||||||
|
if (isInstalled(key)) {
|
||||||
|
return resolveEnsured(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = inFlight.get(key);
|
||||||
|
if (existing) return existing;
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
try {
|
||||||
|
await downloadAndExtract(key, send);
|
||||||
|
return resolveEnsured(key);
|
||||||
|
} finally {
|
||||||
|
inFlight.delete(key);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
inFlight.set(key, promise);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TARGET_TO_TOOLCHAIN: Record<string, ToolchainKey> = {
|
||||||
|
"linux/amd64": "linux-musl-x64",
|
||||||
|
"linux/arm64": "linux-musl-arm64",
|
||||||
|
"linux/arm/v7": "linux-musl-armv7",
|
||||||
|
"windows/amd64": "mingw-w64-x64",
|
||||||
|
"windows/386": "mingw-w64-x86",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toolchainKeyForTarget(targetKey: string): ToolchainKey | null {
|
||||||
|
return TARGET_TO_TOOLCHAIN[targetKey] || null;
|
||||||
|
}
|
||||||
@@ -44,6 +44,9 @@ services:
|
|||||||
OVERLORD_FILE_UPLOAD_INTENT_TTL_MS: ${OVERLORD_FILE_UPLOAD_INTENT_TTL_MS:-1800000}
|
OVERLORD_FILE_UPLOAD_INTENT_TTL_MS: ${OVERLORD_FILE_UPLOAD_INTENT_TTL_MS:-1800000}
|
||||||
OVERLORD_FILE_UPLOAD_PULL_TTL_MS: ${OVERLORD_FILE_UPLOAD_PULL_TTL_MS:-1800000}
|
OVERLORD_FILE_UPLOAD_PULL_TTL_MS: ${OVERLORD_FILE_UPLOAD_PULL_TTL_MS:-1800000}
|
||||||
volumes:
|
volumes:
|
||||||
|
# overlord-data also caches downloaded cross-compile toolchains
|
||||||
|
# under /app/data/toolchains, fetched on first agent build for a
|
||||||
|
# given target (mingw, aarch64/armv7/musl, Android NDK, ldid, UPX).
|
||||||
- overlord-data:/app/data
|
- overlord-data:/app/data
|
||||||
- overlord-certs:/app/certs
|
- overlord-certs:/app/certs
|
||||||
- overlord-client-build-cache:/app/client-build-cache
|
- overlord-client-build-cache:/app/client-build-cache
|
||||||
|
|||||||
Reference in New Issue
Block a user