Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions .github/workflows/texlive-runtime.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
name: Managed XeLaTeX runtimes
on:
push:
branches: [codex/managed-texlive-runtime-v1.2.5]
paths: ['.github/workflows/texlive-runtime.yml', 'scripts/texlive/**', 'App/backend/src/adapters/outbound/plugin-runtime/**', 'plugins/office/scripts/package-runtime.mjs']
workflow_dispatch:
permissions:
contents: read
jobs:
native:
strategy:
fail-fast: false
matrix:
include:
- runner: windows-2025
target: win32-x64
- runner: ubuntu-24.04
target: linux-x64
- runner: ubuntu-24.04-arm
target: linux-arm64
- runner: macos-26
target: darwin-arm64
- runner: macos-15-intel
target: darwin-x64
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 24
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install validation dependencies
run: |
npm ci --ignore-scripts
python -m pip install pymupdf==1.26.5
- name: Test fixture verification and download pinned OSS bundle
run: |
node --test scripts/texlive/fetch-fixtures.test.mjs
node scripts/texlive/fetch-fixtures.mjs "$RUNNER_TEMP/texlive-brand-fixtures" > "$RUNNER_TEMP/texlive-fixtures-verification.json"
- name: Verify and extract pinned official TinyTeX input
run: node scripts/texlive/provision-runtime.mjs "$RUNNER_TEMP/texlive-input"
- name: Provision review packages (Unix release builder only)
if: runner.os != 'Windows'
run: |
TL="$TEXLIVE_BUILD_SOURCE/$TEXLIVE_BUILD_BIN"
export PATH="$TL:$PATH"
"$TL/tlmgr" option repository https://ctan.math.illinois.edu/systems/texlive/tlnet
"$TL/tlmgr" update --self
"$TL/tlmgr" --no-execute-actions update --all
"$TL/tlmgr" --no-execute-actions install xetex ctex fandol fontspec natbib microtype booktabs tools float hyperref geometry xcolor fancyhdr caption colortbl environ trimspaces psnfss lm tex-gyre
"$TL/mktexlsr"
printf 'y\n' | "$TL/updmap-sys" --syncwithtrees --force
"$TL/fmtutil-sys" --byfmt xelatex
- name: Provision review packages (Windows release builder only)
if: runner.os == 'Windows'
shell: pwsh
run: |
$bin = Join-Path $env:TEXLIVE_BUILD_SOURCE $env:TEXLIVE_BUILD_BIN
$tlmgr = Join-Path $bin 'tlmgr.bat'
$env:PATH = "$bin;$env:PATH"
& $tlmgr option repository https://ctan.math.illinois.edu/systems/texlive/tlnet
if ($LASTEXITCODE -ne 0) { throw 'tlmgr repository failed' }
& $tlmgr update --self
if ($LASTEXITCODE -ne 0) { throw 'tlmgr update failed' }
& $tlmgr --no-execute-actions update --all
if ($LASTEXITCODE -ne 0) { throw 'TeX baseline update failed' }
& $tlmgr --no-execute-actions install xetex ctex fandol fontspec natbib microtype booktabs tools float hyperref geometry xcolor fancyhdr caption colortbl environ trimspaces psnfss lm tex-gyre
if ($LASTEXITCODE -ne 0) { throw 'tlmgr package installation failed' }
& (Join-Path $bin 'mktexlsr.exe')
if ($LASTEXITCODE -ne 0) { throw 'TeX index generation failed' }
'y' | & (Join-Path $bin 'updmap-sys.exe') --syncwithtrees --force
if ($LASTEXITCODE -ne 0) { throw 'TeX font map generation failed' }
& (Join-Path $bin 'fmtutil-sys.exe') --byfmt xelatex
if ($LASTEXITCODE -ne 0) { throw 'XeLaTeX format generation failed' }
& (Join-Path $bin 'mktexlsr.exe')
if ($LASTEXITCODE -ne 0) { throw 'TeX index generation failed' }
- name: Prepare relocatable distribution and verified archive
run: |
node scripts/texlive/prepare-runtime.mjs --source "$TEXLIVE_BUILD_SOURCE" --destination "$RUNNER_TEMP/texlive-portable" --bin "$TEXLIVE_BUILD_BIN"
node scripts/texlive/package-runtime.mjs --root "$RUNNER_TEMP/texlive-portable" --destination "$RUNNER_TEMP/texlive-release" --target "${{ matrix.target }}" --version 2026.10.28-2 --tools "$RUNNER_TEMP/texlive-portable/tools.json" --set-default
- name: Hide release source and validate managed cold install and offline reuse
run: |
node --input-type=module -e 'import {rename} from "node:fs/promises"; await rename(process.env.TEXLIVE_BUILD_SOURCE,process.env.TEXLIVE_BUILD_SOURCE+"-disabled");'
node --import tsx scripts/texlive/smoke-install.ts "$RUNNER_TEMP/texlive-release" "$RUNNER_TEMP/texlive-installed" "$RUNNER_TEMP/texlive-env.json" "$RUNNER_TEMP/texlive-brand-fixtures"
- name: Verify PDF brand, Chinese text and render every page
run: python scripts/texlive/verify-pdfs.py "$RUNNER_TEMP/texlive-env.json.qa"
- uses: actions/upload-artifact@v4
if: always()
with:
name: texlive-${{ matrix.target }}-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ runner.temp }}/texlive-release/
${{ runner.temp }}/texlive-env.json.qa/
${{ runner.temp }}/texlive-input/provenance.json
${{ runner.temp }}/texlive-fixtures-verification.json
retention-days: 14
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { createHash } from "node:crypto";
import { fileURLToPath } from "node:url";
import { constants as fsConstants } from "node:fs";
import { access, lstat, mkdir, realpath } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, isAbsolute, relative, resolve, sep, win32 } from "node:path";
import { createInterface } from "node:readline";
import {
Expand All @@ -20,6 +19,7 @@ import { z } from "zod";
import { callTimeoutMs, isCapabilityEvent } from "./shared.js";
import type { PluginAdapter, PluginHostServiceInvoker, PluginRuntimeContext, PluginSession } from "./types.js";
import { createOfficeRuntimeInstaller, type OfficeRuntimeInstaller } from "./office-installer.js";
import { createTexliveRuntimeInstaller, resolveTexliveRuntime, type TexliveRuntimeInstaller } from "./texlive-installer.js";
import { resolvePluginRegistryBaseUrl } from "../../../config/service-urls.js";

const MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
Expand Down Expand Up @@ -83,6 +83,7 @@ export interface CreateCommandPluginAdapterOptions {
pluginDataRoot?: string;
/** Host-created dependency installer; null disables downloading (offline deployments/tests). */
officeRuntimeInstaller?: OfficeRuntimeInstaller | null;
texliveRuntimeInstaller?: TexliveRuntimeInstaller | null;
/** Host-owned services callable over the private command runtime protocol. */
hostServices?: PluginHostServiceInvoker;
/** Resolves whether the signed-in account holds a manifest-declared entitlement. */
Expand Down Expand Up @@ -120,7 +121,8 @@ export function createCommandPluginAdapter(options: CreateCommandPluginAdapterOp
options.fileInputRoots ?? [],
options.pluginDataRoot,
runtimeDependencies.readRoots,
runtimeDependencies.executableRoots
runtimeDependencies.executableRoots,
runtimeDependencies.environment.MEMMY_TEX_CACHE_ROOT
)
));
const allowedNetworkHosts = new Set((options.allowedNetworkHosts ?? []).map((host) => host.trim().toLowerCase()).filter(Boolean));
Expand All @@ -145,7 +147,8 @@ export function createCommandPluginAdapter(options: CreateCommandPluginAdapterOp
code: "plugin_permission_denied"
});
const runtimeDependencies = await resolveCommandRuntimeDependencies(config.runtimeDependencies, platform,
options.officeRuntimeInstaller === null ? undefined : options.officeRuntimeInstaller ?? defaultOfficeInstaller(options.pluginDataRoot));
options.officeRuntimeInstaller === null ? undefined : options.officeRuntimeInstaller ?? defaultOfficeInstaller(options.pluginDataRoot),
options.texliveRuntimeInstaller === null ? undefined : options.texliveRuntimeInstaller ?? defaultTexliveInstaller(options.pluginDataRoot));
const env = resolvePluginEnvironment(
config.env,
config.secretEnv,
Expand Down Expand Up @@ -485,11 +488,13 @@ export async function buildPluginSandboxLaunch(
fileInputRoots: readonly string[] = [],
pluginDataRoot?: string,
runtimeReadRoots: readonly string[] = [],
runtimeExecutableRoots: readonly string[] = []
runtimeExecutableRoots: readonly string[] = [],
runtimeWritableRoot?: string
): Promise<SandboxLaunch> {
const { root, runtimeCommand, runtimeArgs, cwd, interpreter } = await resolveCommandEntrypoint(context, config);
const runtimeRoot = interpreter === "node" ? (platform === "win32" ? dirname(runtimeCommand) : resolve(dirname(runtimeCommand), "..")) : null;
const filesystem = await filesystemRules(context, fileInputRoots, pluginDataRoot);
if (runtimeWritableRoot) filesystem.push({ path: await realpath(runtimeWritableRoot), writable: true });

if (platform === "win32") {
if (networkEnabled) throw new Error("Windows AppContainer plugins currently require no network capability");
Expand Down Expand Up @@ -539,7 +544,8 @@ export interface ResolvedCommandRuntimeDependencies {
export async function resolveCommandRuntimeDependencies(
dependencies: readonly z.infer<typeof CommandRuntimeDependencySchema>[],
platform: NodeJS.Platform = process.platform,
officeInstaller?: OfficeRuntimeInstaller
officeInstaller?: OfficeRuntimeInstaller,
texliveInstaller?: TexliveRuntimeInstaller
): Promise<ResolvedCommandRuntimeDependencies> {
const resolved: ResolvedCommandRuntimeDependencies = { pathEntries: [], readRoots: [], executableRoots: [], environment: {} };
if (dependencies.includes("office")) {
Expand All @@ -549,21 +555,12 @@ export async function resolveCommandRuntimeDependencies(
resolved.executableRoots.push(...office.executableRoots);
Object.assign(resolved.environment, office.environment);
}
if (!dependencies.includes("texlive")) return resolved;

if (platform === "darwin") {
const binaryRoot = await realpath("/Library/TeX/texbin").catch(() => null);
const distributionRoot = await realpath("/usr/local/texlive").catch(() => null);
const userTexmfRoot = await realpath(resolve(homedir(), "Library", "texmf")).catch(() => null);
if (binaryRoot) resolved.pathEntries.push(binaryRoot);
if (distributionRoot) {
resolved.readRoots.push(distributionRoot);
resolved.executableRoots.push(distributionRoot);
}
if (userTexmfRoot) {
resolved.readRoots.push(userTexmfRoot);
resolved.environment.TEXMFHOME = userTexmfRoot;
}
if (dependencies.includes("texlive")) {
const tex = await resolveTexliveRuntime(platform, texliveInstaller);
resolved.pathEntries.push(...tex.pathEntries);
resolved.readRoots.push(...tex.readRoots);
resolved.executableRoots.push(...tex.executableRoots);
Object.assign(resolved.environment, tex.environment);
}
return resolved;
}
Expand Down Expand Up @@ -629,6 +626,16 @@ function defaultOfficeInstaller(pluginDataRoot?: string): OfficeRuntimeInstaller
return createOfficeRuntimeInstaller({ cacheRoot: process.env.MEMMY_OFFICE_RUNTIME_CACHE ?? resolve(dirname(pluginDataRoot), "plugin-runtimes", "office"), catalogUrl });
}

function defaultTexliveInstaller(pluginDataRoot?: string): TexliveRuntimeInstaller | undefined {
if (!pluginDataRoot) return undefined;
const registry = resolvePluginRegistryBaseUrl(process.env);
const catalogUrl = process.env.MEMMY_TEXLIVE_RUNTIME_CATALOG_URL
?? (registry ? new URL("/api/v1/plugins/literature-review/runtimes/{target}", registry).href : undefined);
if (!catalogUrl) return undefined;
return createTexliveRuntimeInstaller({ cacheRoot: process.env.MEMMY_TEXLIVE_RUNTIME_CACHE
?? resolve(dirname(pluginDataRoot), "plugin-runtimes", "texlive"), catalogUrl });
}

interface FilesystemRule {
path: string;
writable: boolean;
Expand Down
Loading
Loading