diff --git a/ts/classes.baseosimagebuilder.ts b/ts/classes.baseosimagebuilder.ts index c49f177..36ec81e 100644 --- a/ts/classes.baseosimagebuilder.ts +++ b/ts/classes.baseosimagebuilder.ts @@ -9,6 +9,7 @@ import * as plugins from './plugins.js'; import type { IBaseOsImageArtifactResult, IBaseOsImageJob, + TBaseOsImageKind, } from './types.js'; export interface IBaseOsImageBuilderOptions { @@ -23,22 +24,22 @@ export class BaseOsImageBuilder { artifact: IBaseOsImageArtifactResult; logs: string[]; }> { - if (jobArg.architecture === 'rpi') { - throw new Error('Raspberry Pi image builds require a raw-image preset and are not supported by the current isocreator ISO pipeline yet'); - } - const logs: string[] = []; + const imageKind = this.getImageKind(jobArg); const jobDir = path.join(this.options.workdir, jobArg.id); const outputDir = path.join(jobDir, 'output'); await fsp.rm(jobDir, { recursive: true, force: true }); await fsp.mkdir(outputDir, { recursive: true }); - const filename = jobArg.architecture === 'amd64' ? 'baseos.iso' : 'baseos-arm64.iso'; + const filename = this.getArtifactFilename(jobArg, imageKind); const outputPath = path.join(outputDir, filename); const configPath = path.join(jobDir, 'isocreator.config.json'); - await fsp.writeFile(configPath, `${JSON.stringify(this.createIsoCreatorConfig(jobArg, outputDir, filename), null, 2)}\n`); + const isoCreatorConfig = imageKind === 'balena-raw' + ? this.createRawImageConfig(jobArg, outputDir, filename) + : this.createIsoCreatorConfig(jobArg, outputDir, filename); + await fsp.writeFile(configPath, `${JSON.stringify(isoCreatorConfig, null, 2)}\n`); - logs.push(`Starting isocreator for ${jobArg.architecture}`); + logs.push(`Starting isocreator for ${jobArg.architecture} ${imageKind}`); await this.runIsoCreator(configPath, logs); const stat = await fsp.stat(outputPath); @@ -51,7 +52,7 @@ export class BaseOsImageBuilder { bucketName: jobArg.s3Descriptor.bucketName, key: jobArg.artifactKey, filename, - contentType: 'application/x-iso9660-image', + contentType: this.getContentType(filename, imageKind), size: stat.size, sha256, createdAt: Date.now(), @@ -66,6 +67,7 @@ export class BaseOsImageBuilder { const envFile = this.createBaseOsEnvFile(jobArg); return { version: '1.0', + imageKind: 'iso', ...(jobArg.sourceImageUrl ? { source: { @@ -136,6 +138,85 @@ export class BaseOsImageBuilder { }; } + private createRawImageConfig(jobArg: IBaseOsImageJob, outputDirArg: string, filenameArg: string) { + if (!jobArg.sourceImageUrl) { + throw new Error('sourceImageUrl is required for balena-raw BaseOS image builds'); + } + + return { + version: '1.0', + imageKind: 'raw-image', + source: { + type: 'url', + url: jobArg.sourceImageUrl, + }, + output: { + filename: filenameArg, + path: outputDirArg, + }, + ...(jobArg.wifi?.ssid + ? { + network: { + wifi: jobArg.wifi, + }, + } + : {}), + raw_image: { + sourceFormat: 'auto', + bootPartition: '/dev/sda1', + outputCompression: filenameArg.endsWith('.xz') ? 'xz' : 'none', + }, + balena_os: { + hostname: jobArg.hostname || `baseos-${jobArg.id.slice(0, 8)}`, + sshPublicKeys: jobArg.sshPublicKey ? [jobArg.sshPublicKey] : [], + configJson: { + serveZone: { + baseos: { + buildId: jobArg.id, + cloudlyUrl: jobArg.cloudlyUrl, + provisioningToken: jobArg.provisioningToken, + }, + }, + }, + baseOsEnv: { + BASEOS_CLOUDLY_URL: jobArg.cloudlyUrl, + BASEOS_JOIN_TOKEN: jobArg.provisioningToken, + BASEOS_STATE_PATH: '/data/baseos/state.json', + BASEOS_HEARTBEAT_INTERVAL_MS: '60000', + SERVEZONE_RUNTIME: 'baseos', + }, + }, + }; + } + + private getImageKind(jobArg: IBaseOsImageJob): TBaseOsImageKind { + if (jobArg.imageKind) { + return jobArg.imageKind; + } + if (jobArg.architecture === 'rpi') { + return 'balena-raw'; + } + if (jobArg.sourceImageUrl && /\.(img|img\.xz|zip)(\?|$)/i.test(jobArg.sourceImageUrl)) { + return 'balena-raw'; + } + return 'ubuntu-iso'; + } + + private getArtifactFilename(jobArg: IBaseOsImageJob, imageKindArg: TBaseOsImageKind) { + const architectureSuffix = jobArg.architecture === 'amd64' ? '' : `-${jobArg.architecture}`; + if (imageKindArg === 'balena-raw') { + return `baseos${architectureSuffix}.img.xz`; + } + return `baseos${architectureSuffix}.iso`; + } + + private getContentType(filenameArg: string, imageKindArg: TBaseOsImageKind) { + if (imageKindArg === 'ubuntu-iso') { + return 'application/x-iso9660-image'; + } + return filenameArg.endsWith('.xz') ? 'application/x-xz' : 'application/octet-stream'; + } + private createBaseOsEnvFile(jobArg: IBaseOsImageJob) { return [ `BASEOS_CLOUDLY_URL=${this.escapeEnvValue(jobArg.cloudlyUrl)}`, diff --git a/ts/classes.corebuildserver.ts b/ts/classes.corebuildserver.ts index 04349fd..23ac41b 100644 --- a/ts/classes.corebuildserver.ts +++ b/ts/classes.corebuildserver.ts @@ -104,7 +104,8 @@ export class CoreBuildServer { return { workerId: this.options.workerId, supportedBuildTypes: ['baseos-image'], - supportedArchitectures: ['amd64', 'arm64'], + supportedArchitectures: ['amd64', 'arm64', 'rpi'], + supportedImageKinds: ['ubuntu-iso', 'balena-raw'], cpuCores: os.cpus().length, memoryGb: Math.round(os.totalmem() / 1024 / 1024 / 1024), workdir: this.options.workdir, diff --git a/ts/types.ts b/ts/types.ts index 62eb384..7aae849 100644 --- a/ts/types.ts +++ b/ts/types.ts @@ -1,6 +1,7 @@ import type { Readable } from 'node:stream'; export type TBaseOsImageArchitecture = 'amd64' | 'arm64' | 'rpi'; +export type TBaseOsImageKind = 'ubuntu-iso' | 'balena-raw'; export interface IS3Descriptor { endpoint: string; @@ -15,6 +16,7 @@ export interface IS3Descriptor { export interface IBaseOsImageJob { id: string; architecture: TBaseOsImageArchitecture; + imageKind?: TBaseOsImageKind; cloudlyUrl: string; provisioningToken: string; sourceImageUrl?: string; @@ -55,6 +57,7 @@ export interface ICoreBuildCapabilities { workerId: string; supportedBuildTypes: string[]; supportedArchitectures: TBaseOsImageArchitecture[]; + supportedImageKinds: TBaseOsImageKind[]; cpuCores: number; memoryGb?: number; workdir: string;