Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
c44ec7454This file is pinned to the selected release. Download the original to open it in your design tool.
import assert from 'node:assert/strict'
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { test } from 'node:test'
import { firmwareDirectory } from './build-output.mjs'
import {
assembleFirmwareBundle,
buildFirmwareBundleTarget,
firmwareBundleName,
firmwareBundleTargetMetadataName,
firmwareBundleTargets,
packageFirmwareBundle,
resolveFirmwareBundleTarget,
} from './firmware-bundle.mjs'
test('bundle targets cover every manifest device and add the custom Stack-chan target', () => {
const manifest = JSON.parse(readFileSync(path.join(firmwareDirectory, 'host/app/manifest.json'), 'utf8'))
const configuredIds = firmwareBundleTargets.map(({ bundleId }) => bundleId)
assert.equal(firmwareBundleName, manifest.bundle.id)
assert.deepEqual(
configuredIds.filter((bundleId) => bundleId.startsWith('com.')).sort(),
[...manifest.bundle.devices].sort(),
)
assert.equal(new Set(configuredIds).size, configuredIds.length)
assert.equal(resolveFirmwareBundleTarget('m5stackchan_cores3').bundleId, 'm5stackchan_cores3')
})
test('builds a release target through a final sdkconfig manifest override', () => {
const root = mkdtempSync(path.join(tmpdir(), 'stackchan-bundle-target-'))
const moddableDirectory = path.join(root, 'moddable')
const outputDirectory = path.join(root, 'output')
const sdkconfigDirectory = path.join(moddableDirectory, 'build/devices/esp32/xsProj-esp32')
const moddableVersion = '8.3.1'
const firmwareVersion = '8.3.1+stackchan.1'
let wrapperManifestPath
try {
mkdirSync(path.join(moddableDirectory, 'tools'), { recursive: true })
mkdirSync(sdkconfigDirectory, { recursive: true })
writeFileSync(path.join(moddableDirectory, 'tools', 'VERSION'), `${moddableVersion}\n`)
writeFileSync(path.join(sdkconfigDirectory, 'sdkconfig.defaults'), 'CONFIG_SPIRAM=y\n')
writeFileSync(path.join(sdkconfigDirectory, 'partitions.csv'), 'fixture partitions\n')
const result = buildFirmwareBundleTarget('m5stack', {
moddableDirectory,
outputDirectory,
runCommand(command, args, cwd, env) {
assert.equal(command, 'mcconfig')
assert.equal(cwd, firmwareDirectory)
assert.equal(env.MODDABLE, moddableDirectory)
assert.equal(args[args.indexOf('-p') + 1], 'esp32/m5stack')
wrapperManifestPath = args.at(-1)
assert.equal(path.dirname(wrapperManifestPath), path.join(firmwareDirectory, 'host/app'))
const wrapper = JSON.parse(readFileSync(wrapperManifestPath, 'utf8'))
assert.equal(wrapper.include[0], path.join(firmwareDirectory, 'host/app/manifest.json'))
const override = JSON.parse(readFileSync(wrapper.include[1], 'utf8'))
assert.equal(override.build.SDKCONFIGPATH, path.join(outputDirectory, 'generated/sdkconfig/m5stack'))
const buildDirectory = path.join(outputDirectory, 'bin/esp32/m5stack/release/stack-chan-host')
mkdirSync(buildDirectory, { recursive: true })
writeFileSync(path.join(buildDirectory, 'bootloader.bin'), Buffer.from([1]))
writePartitionTable(path.join(buildDirectory, 'partition-table.bin'), 256)
writeFirmware(path.join(buildDirectory, 'xs_esp32.bin'), firmwareVersion)
},
})
assert.equal(result.target.bundleId, 'com.m5stack')
assert.equal(result.firmwareVersion, firmwareVersion)
assert.equal(existsSync(wrapperManifestPath), false)
assert.deepEqual(readdirSync(result.directory).sort(), [
'bootloader.bin',
'bundle-target.json',
'partition-table.bin',
'xs_esp32.bin',
])
} finally {
rmSync(root, { recursive: true, force: true })
}
})
test('assembles validated target artifacts without publishing staging metadata', () => {
const fixture = createBundleFixture()
try {
const result = assembleFirmwareBundle({
inputDirectory: fixture.inputDirectory,
outputDirectory: fixture.outputDirectory,
})
assert.equal(result.firmwareVersion, fixture.firmwareVersion)
assert.equal(result.bundleDirectory, path.join(fixture.outputDirectory, firmwareBundleName))
for (const { bundleId } of firmwareBundleTargets) {
assert.deepEqual(readdirSync(path.join(result.bundleDirectory, bundleId)).sort(), [
'bootloader.bin',
'partition-table.bin',
'xs_esp32.bin',
])
}
} finally {
fixture.remove()
}
})
test('creates the distributable archive only after target validation', () => {
const fixture = createBundleFixture()
const commands = []
try {
const result = packageFirmwareBundle({
inputDirectory: fixture.inputDirectory,
outputDirectory: fixture.outputDirectory,
runCommand(command, args, cwd) {
commands.push({ command, args, cwd })
writeFileSync(path.join(cwd, args[1]), 'fixture archive')
},
})
assert.deepEqual(commands, [
{
command: 'zip',
args: ['-r', `${firmwareBundleName}.zip`, firmwareBundleName],
cwd: fixture.outputDirectory,
},
])
assert.equal(readFileSync(result.bundleZipPath, 'utf8'), 'fixture archive')
} finally {
fixture.remove()
}
})
test('rejects incomplete target sets and firmware that exceeds its factory partition', () => {
const incomplete = createBundleFixture()
const oversized = createBundleFixture()
try {
rmSync(path.join(incomplete.inputDirectory, firmwareBundleTargets[0].bundleId), {
recursive: true,
force: true,
})
assert.throws(
() =>
assembleFirmwareBundle({
inputDirectory: incomplete.inputDirectory,
outputDirectory: incomplete.outputDirectory,
}),
/Bundle target set mismatch/,
)
const targetDirectory = path.join(oversized.inputDirectory, firmwareBundleTargets[0].bundleId)
writeFirmware(path.join(targetDirectory, 'xs_esp32.bin'), oversized.firmwareVersion, 320)
assert.throws(
() =>
assembleFirmwareBundle({
inputDirectory: oversized.inputDirectory,
outputDirectory: oversized.outputDirectory,
}),
/Firmware exceeds factory app partition/,
)
} finally {
incomplete.remove()
oversized.remove()
}
})
test('rejects inconsistent embedded versions across independently built targets', () => {
const fixture = createBundleFixture()
try {
const { bundleId } = firmwareBundleTargets[0]
const targetDirectory = path.join(fixture.inputDirectory, bundleId)
writeFirmware(path.join(targetDirectory, 'xs_esp32.bin'), '9.9.9')
writeMetadata(targetDirectory, bundleId, '9.9.9')
assert.throws(
() =>
assembleFirmwareBundle({
inputDirectory: fixture.inputDirectory,
outputDirectory: fixture.outputDirectory,
}),
/Bundle targets use different firmware versions/,
)
} finally {
fixture.remove()
}
})
function createBundleFixture() {
const root = mkdtempSync(path.join(tmpdir(), 'stackchan-firmware-bundle-'))
const inputDirectory = path.join(root, 'input')
const outputDirectory = path.join(root, 'output')
const firmwareVersion = '8.3.1'
mkdirSync(inputDirectory, { recursive: true })
mkdirSync(outputDirectory, { recursive: true })
for (const { bundleId } of firmwareBundleTargets) {
const targetDirectory = path.join(inputDirectory, bundleId)
mkdirSync(targetDirectory, { recursive: true })
writeFileSync(path.join(targetDirectory, 'bootloader.bin'), Buffer.from([1]))
writePartitionTable(path.join(targetDirectory, 'partition-table.bin'), 256)
writeFirmware(path.join(targetDirectory, 'xs_esp32.bin'), firmwareVersion)
writeMetadata(targetDirectory, bundleId, firmwareVersion)
}
return {
inputDirectory,
outputDirectory,
firmwareVersion,
remove() {
rmSync(root, { recursive: true, force: true })
},
}
}
function writePartitionTable(filePath, factorySize) {
const partitionTable = Buffer.alloc(64)
partitionTable.writeUInt16LE(0x50aa, 0)
partitionTable.writeUInt8(0, 2)
partitionTable.writeUInt8(0, 3)
partitionTable.writeUInt32LE(0x10000, 4)
partitionTable.writeUInt32LE(factorySize, 8)
writeFileSync(filePath, partitionTable)
}
function writeFirmware(filePath, version, size = 128) {
const firmware = Buffer.alloc(size)
firmware.writeUInt32LE(0xabcd5432, 0x20)
firmware.write(version, 0x30, 'utf8')
writeFileSync(filePath, firmware)
}
function writeMetadata(directory, target, firmwareVersion) {
writeFileSync(
path.join(directory, firmwareBundleTargetMetadataName),
JSON.stringify({
format: 'tech.moddable.stackchan.bundle-target',
formatVersion: 1,
target,
firmwareVersion,
}),
)
}