Integrate backend sidecar process and external binaries into the Tauri build
This commit is contained in:
27
backend/sidecar_main.py
Normal file
27
backend/sidecar_main.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
multiprocessing.freeze_support()
|
||||||
|
os.environ.setdefault("HEIMGEIST_BACKEND_SIDECAR", "1")
|
||||||
|
host = os.getenv("HEIMGEIST_BACKEND_HOST", "127.0.0.1")
|
||||||
|
port = int(os.getenv("HEIMGEIST_BACKEND_PORT", "8000"))
|
||||||
|
log_level = os.getenv("HEIMGEIST_BACKEND_LOG_LEVEL", "info")
|
||||||
|
|
||||||
|
uvicorn.run(
|
||||||
|
"backend.main:app",
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
log_level=log_level,
|
||||||
|
reload=False,
|
||||||
|
workers=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -11,6 +11,9 @@
|
|||||||
"dev:tauri:shell": "tauri dev",
|
"dev:tauri:shell": "tauri dev",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"build:sidecar": "node scripts/build-backend-sidecar.cjs",
|
||||||
|
"package:mac": "npm run build:sidecar && tauri build --bundles app",
|
||||||
|
"build:app": "npm run package:mac",
|
||||||
"start": "npm run dev"
|
"start": "npm run dev"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
177
scripts/build-backend-sidecar.cjs
Normal file
177
scripts/build-backend-sidecar.cjs
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
const { execFileSync, spawnSync } = require('child_process')
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const projectRoot = path.resolve(__dirname, '..')
|
||||||
|
const pythonBin = path.join(projectRoot, 'backend', '.venv', 'bin', 'python')
|
||||||
|
const binariesDir = path.join(projectRoot, 'src-tauri', 'binaries')
|
||||||
|
const targetTriple = resolveTargetTriple()
|
||||||
|
const executableExtension = process.platform === 'win32' ? '.exe' : ''
|
||||||
|
const backendOutputName = `heimgeist-backend-${targetTriple}${executableExtension}`
|
||||||
|
const backendOutputPath = path.join(binariesDir, backendOutputName)
|
||||||
|
|
||||||
|
function resolveTargetTriple() {
|
||||||
|
const output = execFileSync('rustc', ['-vV'], { encoding: 'utf8' })
|
||||||
|
const match = output.match(/^host:\s+(\S+)/m)
|
||||||
|
if (!match) {
|
||||||
|
throw new Error('Could not determine Rust target triple from rustc -vV output.')
|
||||||
|
}
|
||||||
|
return match[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(command, args, options = {}) {
|
||||||
|
const result = spawnSync(command, args, {
|
||||||
|
cwd: projectRoot,
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: process.env,
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensurePython() {
|
||||||
|
if (!fs.existsSync(pythonBin)) {
|
||||||
|
throw new Error(`Missing backend Python virtualenv at ${pythonBin}. Run ./run.sh or create backend/.venv first.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensurePyInstaller() {
|
||||||
|
const result = spawnSync(pythonBin, ['-c', 'import PyInstaller'], {
|
||||||
|
cwd: projectRoot,
|
||||||
|
stdio: 'ignore',
|
||||||
|
})
|
||||||
|
if (result.status === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Installing PyInstaller into backend/.venv for sidecar packaging.')
|
||||||
|
run(pythonBin, ['-m', 'pip', 'install', '--upgrade', 'pyinstaller'])
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyExecutable(sourcePath, outputBaseName) {
|
||||||
|
if (!sourcePath || !fs.existsSync(sourcePath)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
fs.mkdirSync(binariesDir, { recursive: true })
|
||||||
|
const outputPath = path.join(binariesDir, `${outputBaseName}-${targetTriple}${executableExtension}`)
|
||||||
|
fs.copyFileSync(sourcePath, outputPath)
|
||||||
|
fs.chmodSync(outputPath, 0o755)
|
||||||
|
return outputPath
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeCopyFfmpeg() {
|
||||||
|
try {
|
||||||
|
const ffmpegPath = require('ffmpeg-static')
|
||||||
|
const outputPath = copyExecutable(ffmpegPath, 'heimgeist-ffmpeg')
|
||||||
|
if (outputPath) {
|
||||||
|
console.log(`Bundled ffmpeg helper: ${outputPath}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Could not bundle ffmpeg-static: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeCopyFfprobe() {
|
||||||
|
try {
|
||||||
|
const ffprobeStatic = require('ffprobe-static')
|
||||||
|
const ffprobePath = ffprobeStatic && ffprobeStatic.path
|
||||||
|
if (!ffprobePath || !fs.existsSync(ffprobePath)) {
|
||||||
|
console.warn('Could not bundle ffprobe-static: package did not provide a binary path.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const probe = spawnSync(ffprobePath, ['-version'], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: 5000,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
})
|
||||||
|
if (probe.status !== 0) {
|
||||||
|
console.warn(
|
||||||
|
`Skipping bundled ffprobe: ${ffprobePath} did not run successfully on this machine. Audio/video duration probing remains a release gap.`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputPath = copyExecutable(ffprobePath, 'heimgeist-ffprobe')
|
||||||
|
if (outputPath) {
|
||||||
|
console.log(`Bundled ffprobe helper: ${outputPath}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Could not bundle ffprobe-static: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeBinariesIgnore() {
|
||||||
|
fs.mkdirSync(binariesDir, { recursive: true })
|
||||||
|
const ignorePath = path.join(binariesDir, '.gitignore')
|
||||||
|
if (!fs.existsSync(ignorePath)) {
|
||||||
|
fs.writeFileSync(ignorePath, '*\n!.gitignore\n', 'utf8')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBackendSidecar() {
|
||||||
|
const pyinstallerRoot = path.join('/tmp', `heimgeist-pyinstaller-${targetTriple}`)
|
||||||
|
const distPath = path.join(pyinstallerRoot, 'dist')
|
||||||
|
const workPath = path.join(pyinstallerRoot, 'build')
|
||||||
|
const specPath = path.join(pyinstallerRoot, 'spec')
|
||||||
|
|
||||||
|
fs.rmSync(pyinstallerRoot, { recursive: true, force: true })
|
||||||
|
fs.rmSync(backendOutputPath, { force: true })
|
||||||
|
fs.mkdirSync(distPath, { recursive: true })
|
||||||
|
fs.mkdirSync(workPath, { recursive: true })
|
||||||
|
fs.mkdirSync(specPath, { recursive: true })
|
||||||
|
|
||||||
|
run(pythonBin, [
|
||||||
|
'-m',
|
||||||
|
'PyInstaller',
|
||||||
|
'--noconfirm',
|
||||||
|
'--clean',
|
||||||
|
'--onefile',
|
||||||
|
'--name',
|
||||||
|
'heimgeist-backend',
|
||||||
|
'--distpath',
|
||||||
|
distPath,
|
||||||
|
'--workpath',
|
||||||
|
workPath,
|
||||||
|
'--specpath',
|
||||||
|
specPath,
|
||||||
|
'--paths',
|
||||||
|
projectRoot,
|
||||||
|
'--collect-submodules',
|
||||||
|
'backend.rag',
|
||||||
|
'--hidden-import',
|
||||||
|
'backend.main',
|
||||||
|
'--hidden-import',
|
||||||
|
'backend.sidecar_main',
|
||||||
|
'--hidden-import',
|
||||||
|
'uvicorn.logging',
|
||||||
|
'--hidden-import',
|
||||||
|
'uvicorn.loops.auto',
|
||||||
|
'--hidden-import',
|
||||||
|
'uvicorn.protocols.http.auto',
|
||||||
|
'--hidden-import',
|
||||||
|
'uvicorn.protocols.websockets.auto',
|
||||||
|
'--hidden-import',
|
||||||
|
'uvicorn.lifespan.on',
|
||||||
|
path.join(projectRoot, 'backend', 'sidecar_main.py'),
|
||||||
|
])
|
||||||
|
|
||||||
|
const builtBinary = path.join(distPath, `heimgeist-backend${executableExtension}`)
|
||||||
|
if (!fs.existsSync(builtBinary)) {
|
||||||
|
throw new Error(`PyInstaller did not produce expected binary at ${builtBinary}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(binariesDir, { recursive: true })
|
||||||
|
fs.copyFileSync(builtBinary, backendOutputPath)
|
||||||
|
fs.chmodSync(backendOutputPath, 0o755)
|
||||||
|
console.log(`Built backend sidecar: ${backendOutputPath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
ensurePython()
|
||||||
|
ensurePyInstaller()
|
||||||
|
writeBinariesIgnore()
|
||||||
|
buildBackendSidecar()
|
||||||
|
maybeCopyFfmpeg()
|
||||||
|
maybeCopyFfprobe()
|
||||||
@@ -15,3 +15,4 @@ serde_json = "1"
|
|||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
|
tauri-plugin-shell = "2"
|
||||||
|
|||||||
2
src-tauri/binaries/.gitignore
vendored
Normal file
2
src-tauri/binaries/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -26,8 +26,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": false,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": ["app"],
|
||||||
"icon": []
|
"icon": ["icons/icon.png"],
|
||||||
|
"externalBin": [
|
||||||
|
"binaries/heimgeist-backend",
|
||||||
|
"binaries/heimgeist-ffmpeg"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user