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
5 changes: 0 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,6 @@ resources/st-compiler/**.spec

# External tool binaries (downloaded by scripts/download-binaries.ts)
# arduino-cli stays committed since we don't own its releases
resources/bin/**/xml2st
resources/bin/**/xml2st.exe
resources/bin/**/xml2st/
resources/bin/.binary-metadata.json
resources/bin/**/.binary-metadata.json
resources/strucpp/

# Playwright
Expand Down
6 changes: 1 addition & 5 deletions binary-versions.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
{
"xml2st": {
"version": "v4.0.5",
"repository": "Autonomy-Logic/xml2st"
},
"strucpp": {
"version": "v0.5.5",
"repository": "Autonomy-Logic/STruCpp"
}
}
}
20 changes: 15 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@tailwindcss/forms": "^0.5.10",
"@tanstack/react-query": "^5.90.21",
"@tanstack/react-table": "^8.21.2",
"@xmldom/xmldom": "^0.9.10",
"@xyflow/react": "^12.0.1",
"auto-zustand-selectors-hook": "^2.0.0",
"avr8js": "0.20.0",
Expand Down
237 changes: 15 additions & 222 deletions scripts/download-binaries.ts
Original file line number Diff line number Diff line change
@@ -1,115 +1,35 @@
/**
* Download external tool binaries (xml2st, strucpp) from GitHub Releases.
* Download external tool binaries (strucpp) from GitHub Releases.
*
* Usage:
* ts-node scripts/download-binaries.ts [--platform <platform>] [--arch <arch>] [--force]
* ts-node scripts/download-binaries.ts [--force]
*
* Defaults to the current platform/arch. Use --force to re-download even if cached.
* Use --force to re-install even if cached.
*
* The legacy `xml2st` binary download path was removed when the
* editor migrated to the in-process JSON → ST transpiler
* (`backend/shared/transpilers/generate-st-from-json/`).
*/

import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

interface ToolEntry {
version: string
repository: string
}

interface BinaryVersions {
xml2st: ToolEntry
strucpp: ToolEntry
}

interface CacheMetadata {
xml2st: string
platform: string
arch: string
}

type Platform = 'darwin' | 'linux' | 'win32'
type Arch = 'x64' | 'arm64'

// ---------------------------------------------------------------------------
// Paths
// ---------------------------------------------------------------------------

const ROOT_DIR = path.resolve(__dirname, '..')
const VERSIONS_FILE = path.join(ROOT_DIR, 'binary-versions.json')
const RESOURCES_DIR = path.join(ROOT_DIR, 'resources')

function binDir(platform: Platform, arch: Arch): string {
return path.join(RESOURCES_DIR, 'bin', platform, arch)
}

function cacheFile(platform: Platform, arch: Arch): string {
return path.join(binDir(platform, arch), '.binary-metadata.json')
}

// ---------------------------------------------------------------------------
// CLI argument parsing
// ---------------------------------------------------------------------------

function parseArgs(): { platform: Platform; arch: Arch; force: boolean } {
const args = process.argv.slice(2)
let platform = process.platform as string
let arch = process.arch as string
let force = false

for (let i = 0; i < args.length; i++) {
if (args[i] === '--platform' && args[i + 1]) {
platform = args[++i]
} else if (args[i] === '--arch' && args[i + 1]) {
arch = args[++i]
} else if (args[i] === '--force') {
force = true
}
}

if (!['darwin', 'linux', 'win32'].includes(platform)) {
console.error(`Unsupported platform: ${platform}`)
process.exit(1)
}
if (!['x64', 'arm64'].includes(arch)) {
console.error(`Unsupported architecture: ${arch}`)
process.exit(1)
}

return { platform: platform as Platform, arch: arch as Arch, force }
}

// ---------------------------------------------------------------------------
// Cache check
// ---------------------------------------------------------------------------

function getCachedMetadata(platform: Platform, arch: Arch): CacheMetadata | null {
const file = cacheFile(platform, arch)
if (!fs.existsSync(file)) return null

try {
return JSON.parse(fs.readFileSync(file, 'utf-8')) as CacheMetadata
} catch {
return null
}
}

function needsXml2st(versions: BinaryVersions, cached: CacheMetadata | null, platform: Platform, arch: Arch): boolean {
const dir = binDir(platform, arch)
const isWindows = platform === 'win32'
const isDarwin = platform === 'darwin'

const xml2stPath = isDarwin
? path.join(dir, 'xml2st', 'xml2st')
: path.join(dir, isWindows ? 'xml2st.exe' : 'xml2st')

if (!fs.existsSync(xml2stPath)) return true
if (!cached || cached.xml2st !== versions.xml2st.version) return true

return false
function parseArgs(): { force: boolean } {
return { force: process.argv.slice(2).includes('--force') }
}

function needsStrucpp(versions: BinaryVersions): boolean {
Expand All @@ -126,24 +46,9 @@ function needsStrucpp(versions: BinaryVersions): boolean {
} catch {
return true
}

return false
}

function writeCache(versions: BinaryVersions, platform: Platform, arch: Arch): void {
const data: CacheMetadata = {
xml2st: versions.xml2st.version,
platform,
arch,
}
fs.mkdirSync(path.dirname(cacheFile(platform, arch)), { recursive: true })
fs.writeFileSync(cacheFile(platform, arch), JSON.stringify(data, null, 2) + '\n')
}

// ---------------------------------------------------------------------------
// Download helpers
// ---------------------------------------------------------------------------

async function downloadToFile(url: string, dest: string): Promise<void> {
const response = await fetch(url, { redirect: 'follow' })
if (!response.ok) {
Expand All @@ -153,101 +58,12 @@ async function downloadToFile(url: string, dest: string): Promise<void> {
fs.writeFileSync(dest, new Uint8Array(arrayBuffer))
}

function extractTarGz(archive: string, destDir: string): void {
fs.mkdirSync(destDir, { recursive: true })
execSync(`tar xzf "${archive}" -C "${destDir}"`, { stdio: 'pipe' })
}

function extractZip(archive: string, destDir: string): void {
fs.mkdirSync(destDir, { recursive: true })
execSync(`tar xf "${archive}" -C "${destDir}"`, { stdio: 'pipe' })
}

function rmrf(p: string): void {
if (fs.existsSync(p)) {
fs.rmSync(p, { recursive: true, force: true })
}
}

function copyRecursive(src: string, dest: string): void {
fs.mkdirSync(dest, { recursive: true })
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
const srcPath = path.join(src, entry.name)
const destPath = path.join(dest, entry.name)
if (entry.isSymbolicLink()) {
const linkTarget = fs.readlinkSync(srcPath)
if (fs.existsSync(destPath)) fs.rmSync(destPath, { force: true })
fs.symlinkSync(linkTarget, destPath)
} else if (entry.isDirectory()) {
copyRecursive(srcPath, destPath)
} else {
fs.copyFileSync(srcPath, destPath)
}
}
}

// ---------------------------------------------------------------------------
// xml2st download and extraction
// ---------------------------------------------------------------------------

async function downloadXml2st(
tool: ToolEntry,
platform: Platform,
arch: Arch,
targetBinDir: string,
): Promise<void> {
const isWindows = platform === 'win32'
const isDarwin = platform === 'darwin'
const ext = isWindows ? 'zip' : 'tar.gz'
const url = `https://github.com/${tool.repository}/releases/download/${tool.version}/xml2st-${platform}-${arch}.${ext}`

console.log(` Downloading xml2st ${tool.version} for ${platform}-${arch}...`)
const tmpDir = fs.mkdtempSync(path.join(RESOURCES_DIR, '.tmp-xml2st-'))

try {
const archivePath = path.join(tmpDir, `xml2st.${ext}`)
await downloadToFile(url, archivePath)

const extractDir = path.join(tmpDir, 'extracted')
if (isWindows) {
extractZip(archivePath, extractDir)
} else {
extractTarGz(archivePath, extractDir)
}

// Archive contains xml2st/ directory
const extractedToolDir = path.join(extractDir, 'xml2st')

if (isDarwin) {
// macOS: xml2st is a directory with _internal/ — copy as-is
const destDir = path.join(targetBinDir, 'xml2st')
rmrf(destDir)
copyRecursive(extractedToolDir, destDir)
fs.chmodSync(path.join(destDir, 'xml2st'), 0o755)
} else {
// Linux/Windows: single executable
const exeName = isWindows ? 'xml2st.exe' : 'xml2st'
const srcFile = path.join(extractedToolDir, exeName)
const destFile = path.join(targetBinDir, exeName)
rmrf(destFile)
// Also remove any leftover directory from a previous macOS-style install
rmrf(path.join(targetBinDir, 'xml2st'))
fs.copyFileSync(srcFile, destFile)
if (!isWindows) {
fs.chmodSync(destFile, 0o755)
}
}

console.log(` xml2st ${tool.version} installed.`)
} finally {
rmrf(tmpDir)
}
}

// ---------------------------------------------------------------------------
// strucpp download and extraction
// ---------------------------------------------------------------------------

async function downloadStrucpp(tool: ToolEntry): Promise<void> {
// The npm tarball is platform-independent (pure TypeScript + C++ headers)
const version = tool.version.replace(/^v/, '')
Expand Down Expand Up @@ -284,12 +100,8 @@ async function downloadStrucpp(tool: ToolEntry): Promise<void> {
}
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

async function main(): Promise<void> {
const { platform, arch, force } = parseArgs()
const { force } = parseArgs()

if (!fs.existsSync(VERSIONS_FILE)) {
console.error(`binary-versions.json not found at ${VERSIONS_FILE}`)
Expand All @@ -298,34 +110,15 @@ async function main(): Promise<void> {

const versions: BinaryVersions = JSON.parse(fs.readFileSync(VERSIONS_FILE, 'utf-8'))

console.log(`[download-binaries] platform=${platform} arch=${arch} force=${force}`)
console.log(`[download-binaries] force=${force}`)

const targetBinDir = binDir(platform, arch)
fs.mkdirSync(targetBinDir, { recursive: true })

const cached = force ? null : getCachedMetadata(platform, arch)
const downloadXml2stNeeded = force || needsXml2st(versions, cached, platform, arch)
const downloadStrucppNeeded = force || needsStrucpp(versions)

if (!downloadXml2stNeeded && !downloadStrucppNeeded) {
console.log(`[download-binaries] All tools up to date, skipping.`)
return
}

if (downloadXml2stNeeded) {
await downloadXml2st(versions.xml2st, platform, arch, targetBinDir)
} else {
console.log(` xml2st ${versions.xml2st.version} already installed, skipping.`)
}

// strucpp is platform-independent — only download once regardless of platform/arch
if (downloadStrucppNeeded) {
await downloadStrucpp(versions.strucpp)
} else {
if (!force && !needsStrucpp(versions)) {
console.log(` strucpp ${versions.strucpp.version} already installed, skipping.`)
console.log(`[download-binaries] Done.`)
return
}

writeCache(versions, platform, arch)
await downloadStrucpp(versions.strucpp)
console.log(`[download-binaries] Done.`)
}

Expand Down
Loading
Loading