@geastack/cli 0.1.52 → 0.1.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/SPEC.md +2 -2
- package/package.json +2 -3
- package/src/apps/app-index-writer.mjs +15 -0
- package/src/apps/apple-icons.mjs +147 -0
- package/src/apps/bundle-writer.mjs +156 -0
- package/src/apps/launcher-catalog.mjs +114 -0
- package/src/apps/openai-icons.mjs +356 -0
- package/src/apps/zip-writer.mjs +104 -0
- package/src/ble/ble-ota.swift +291 -0
- package/src/boards/config.mjs +32 -0
- package/src/boards/custom-target.mjs +309 -0
- package/src/boards/resolve.mjs +143 -0
- package/src/boards/targets.mjs +45 -0
- package/src/boards/usb.mjs +211 -0
- package/src/commands/apps.mjs +236 -0
- package/src/commands/board.mjs +402 -0
- package/src/commands/doctor.mjs +91 -0
- package/src/context.mjs +45 -52
- package/src/device/device.mjs +96 -0
- package/src/device/image.mjs +115 -0
- package/src/device/serial.mjs +430 -0
- package/src/device/wifi.mjs +190 -0
- package/src/esp32/build.mjs +321 -0
- package/src/esp32/capabilities.mjs +54 -0
- package/src/esp32/flash.mjs +181 -0
- package/src/esp32/idf-env.mjs +171 -0
- package/src/esp32/ota.mjs +80 -0
- package/src/esp32/partitions.mjs +59 -0
- package/src/esp32/sdkconfig.mjs +103 -0
- package/src/esp32/wifi-config.mjs +72 -0
- package/src/gea.mjs +86 -427
- package/src/geaos/adapter.mjs +119 -0
- package/src/heap-report.mjs +1276 -0
- package/src/manifest.mjs +135 -36
- package/src/rp2350/adapter.mjs +155 -0
- package/src/setup-wizard.mjs +6 -10
- package/src/taurus/adapter.mjs +52 -0
package/src/manifest.mjs
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
|
|
3
|
+
import { loadBoardConfig, normalizeBoardConfig } from './boards/config.mjs'
|
|
4
|
+
import { loadTargets } from './boards/targets.mjs'
|
|
3
5
|
import { CliError, ExitCode, fail } from './errors.mjs'
|
|
4
6
|
import { exists, findUp, isDirectory, listDirectories, readJson } from './fs-utils.mjs'
|
|
5
7
|
|
|
8
|
+
// App discovery. A Gea app is a package.json with a `gea` block; the project
|
|
9
|
+
// root, its apps/ and examples/ children, and any GEA_EXTRA_APP_DIRS entries
|
|
10
|
+
// are searched. The normalized shape is the one every consumer (IDF, the
|
|
11
|
+
// Pico SDK, geaos, the bundle writer, the launcher generator) reads.
|
|
12
|
+
|
|
13
|
+
export function readPathList(value) {
|
|
14
|
+
return String(value || '').split(path.delimiter).map((entry) => entry.trim()).filter(Boolean)
|
|
15
|
+
}
|
|
16
|
+
|
|
6
17
|
export function discoverApps(ctx) {
|
|
7
|
-
const roots = [ctx.projectRoot]
|
|
18
|
+
const roots = [ctx.projectRoot, ...readPathList(ctx.env?.GEA_EXTRA_APP_DIRS)]
|
|
8
19
|
const seen = new Set()
|
|
9
20
|
const apps = []
|
|
10
21
|
for (const root of roots) {
|
|
11
22
|
for (const app of discoverAppsInRoot(root)) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
seen.add(key)
|
|
23
|
+
if (seen.has(app.root) || apps.some((known) => known.id === app.id)) continue
|
|
24
|
+
seen.add(app.root)
|
|
15
25
|
apps.push(app)
|
|
16
26
|
}
|
|
17
27
|
}
|
|
18
|
-
return apps.sort((a, b) => a.id.localeCompare(b.id))
|
|
28
|
+
return apps.sort((a, b) => a.launcher.order - b.launcher.order || a.id.localeCompare(b.id))
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
export function discoverAppsInRoot(root) {
|
|
@@ -37,16 +47,17 @@ export function discoverAppsInRoot(root) {
|
|
|
37
47
|
})
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
export function resolveRequestedApp(ctx, parsed, positionals) {
|
|
50
|
+
export function resolveRequestedApp(ctx, parsed, positionals = []) {
|
|
41
51
|
const requested = parsed.options.app || positionals[0]
|
|
42
52
|
if (requested) {
|
|
43
|
-
const
|
|
44
|
-
|
|
53
|
+
const id = String(Array.isArray(requested) ? requested.at(-1) : requested)
|
|
54
|
+
const app = findAppById(ctx, id)
|
|
55
|
+
if (!app) fail(`Could not find Gea app '${id}' in ${ctx.projectRoot}.`, ExitCode.usage)
|
|
45
56
|
return app
|
|
46
57
|
}
|
|
47
58
|
const current = findCurrentApp(ctx.cwd)
|
|
48
59
|
if (current) return current
|
|
49
|
-
fail('No app selected. Pass --app <id
|
|
60
|
+
fail('No app selected. Pass --app <id> or run inside a Gea app folder.', ExitCode.usage)
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
export function findAppById(ctx, id) {
|
|
@@ -54,7 +65,7 @@ export function findAppById(ctx, id) {
|
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
export function findCurrentApp(cwd) {
|
|
57
|
-
const packageDir = findUp(cwd, (dir) =>
|
|
68
|
+
const packageDir = findUp(cwd, (dir) => hasGeaManifest(path.join(dir, 'package.json')))
|
|
58
69
|
if (!packageDir) return null
|
|
59
70
|
return normalizeApp(packageDir, readJson(path.join(packageDir, 'package.json')))
|
|
60
71
|
}
|
|
@@ -66,6 +77,9 @@ export function validateApp(app) {
|
|
|
66
77
|
if (app.entry && !exists(path.join(app.root, app.entry))) errors.push(`gea.entry does not exist: ${app.entry}`)
|
|
67
78
|
if (!app.runtime) errors.push('gea.runtime is required or must default to gea')
|
|
68
79
|
if (!app.targets || typeof app.targets !== 'object' || Array.isArray(app.targets)) errors.push('gea.targets must be an object')
|
|
80
|
+
for (const source of app.nativeSources) {
|
|
81
|
+
if (!exists(path.join(app.root, source))) errors.push(`gea.nativeSources entry does not exist: ${source}`)
|
|
82
|
+
}
|
|
69
83
|
return errors
|
|
70
84
|
}
|
|
71
85
|
|
|
@@ -78,8 +92,7 @@ export function assertValidApp(app) {
|
|
|
78
92
|
|
|
79
93
|
export function targetEnabledForApp(ctx, app, targetOrPlatform) {
|
|
80
94
|
if (!targetOrPlatform) return true
|
|
81
|
-
|
|
82
|
-
return platforms.some((platform) => app.targets?.[platform] === true)
|
|
95
|
+
return appPlatformsForTarget(ctx, targetOrPlatform).some((platform) => app.targets?.[platform] === true)
|
|
83
96
|
}
|
|
84
97
|
|
|
85
98
|
export function assertTargetEnabled(ctx, app, targetOrPlatform) {
|
|
@@ -89,16 +102,18 @@ export function assertTargetEnabled(ctx, app, targetOrPlatform) {
|
|
|
89
102
|
}
|
|
90
103
|
}
|
|
91
104
|
|
|
105
|
+
export const knownPlatforms = Object.freeze(['web', 'esp32', 'rp2350', 'geaos', 'macos', 'ios', 'android'])
|
|
106
|
+
|
|
92
107
|
export function appPlatformForTarget(ctx, targetOrBoard) {
|
|
93
108
|
return appPlatformsForTarget(ctx, targetOrBoard)[0] || ''
|
|
94
109
|
}
|
|
95
110
|
|
|
96
111
|
export function appPlatformsForTarget(ctx, targetOrBoard) {
|
|
97
112
|
if (!targetOrBoard) return []
|
|
98
|
-
if (
|
|
99
|
-
const targets =
|
|
113
|
+
if (knownPlatforms.includes(targetOrBoard)) return [targetOrBoard]
|
|
114
|
+
const targets = safeTargets(ctx)
|
|
100
115
|
if (targets[targetOrBoard]?.appPlatform) return targetAppPlatforms(targets[targetOrBoard])
|
|
101
|
-
const boards =
|
|
116
|
+
const boards = safeBoards(ctx)
|
|
102
117
|
const board = boards[targetOrBoard]
|
|
103
118
|
const boardTarget = board?.target
|
|
104
119
|
if (board?.appPlatform) {
|
|
@@ -112,25 +127,54 @@ export function appPlatformsForTarget(ctx, targetOrBoard) {
|
|
|
112
127
|
return [targetOrBoard]
|
|
113
128
|
}
|
|
114
129
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
130
|
+
// Consumers (CMake, the apple/geaos build scripts) join a relative root onto
|
|
131
|
+
// GEA_APPS_ROOT; an app outside the project keeps its absolute path.
|
|
132
|
+
export function appRootFor(ctx, app) {
|
|
133
|
+
const relative = path.relative(ctx.projectRoot, app.root)
|
|
134
|
+
if (!relative) return '.'
|
|
135
|
+
return relative.startsWith('..') || path.isAbsolute(relative) ? app.root : relative.split(path.sep).join('/')
|
|
118
136
|
}
|
|
119
137
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return
|
|
138
|
+
// `root;entry;runtime;nativeSource...` -- the one line CMake splits on ';'.
|
|
139
|
+
export function appCmakeMeta(ctx, app) {
|
|
140
|
+
return [appRootFor(ctx, app), app.entry, app.runtime, ...app.nativeSources].join(';')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function appSummary(ctx, app) {
|
|
144
|
+
return {
|
|
145
|
+
id: app.id,
|
|
146
|
+
name: app.name,
|
|
147
|
+
packageName: app.packageName,
|
|
148
|
+
version: app.version,
|
|
149
|
+
root: appRootFor(ctx, app),
|
|
150
|
+
entry: app.entry,
|
|
151
|
+
runtime: app.runtime,
|
|
152
|
+
targets: app.targets,
|
|
153
|
+
icons: app.icons,
|
|
154
|
+
nativeSources: app.nativeSources,
|
|
155
|
+
launcher: app.launcher
|
|
156
|
+
}
|
|
123
157
|
}
|
|
124
158
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
159
|
+
function safeTargets(ctx) {
|
|
160
|
+
try {
|
|
161
|
+
return loadTargets(ctx)
|
|
162
|
+
} catch {
|
|
163
|
+
return {}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function safeBoards(ctx) {
|
|
168
|
+
try {
|
|
169
|
+
return normalizeBoardConfig(loadBoardConfig(ctx))
|
|
170
|
+
} catch {
|
|
171
|
+
return {}
|
|
172
|
+
}
|
|
129
173
|
}
|
|
130
174
|
|
|
131
175
|
function hasGeaManifest(packagePath) {
|
|
132
176
|
try {
|
|
133
|
-
return Boolean(readJson(packagePath).gea)
|
|
177
|
+
return exists(packagePath) && Boolean(readJson(packagePath).gea)
|
|
134
178
|
} catch {
|
|
135
179
|
return false
|
|
136
180
|
}
|
|
@@ -152,19 +196,74 @@ function uniquePlatforms(values) {
|
|
|
152
196
|
return out
|
|
153
197
|
}
|
|
154
198
|
|
|
155
|
-
function
|
|
199
|
+
function packageId(packageName) {
|
|
200
|
+
const last = String(packageName || '').split('/').pop() || ''
|
|
201
|
+
return last.replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'app'
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function normalizeTargets(raw) {
|
|
205
|
+
if (Array.isArray(raw)) return Object.fromEntries(raw.map((name) => [String(name), true]))
|
|
206
|
+
if (!raw || typeof raw !== 'object') return {}
|
|
207
|
+
const out = {}
|
|
208
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
209
|
+
if (!name) continue
|
|
210
|
+
if (value && typeof value === 'object') out[name] = value.enabled !== false
|
|
211
|
+
else out[name] = value === true
|
|
212
|
+
}
|
|
213
|
+
return out
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizeIcons(raw) {
|
|
217
|
+
if (!raw || typeof raw !== 'object') return {}
|
|
218
|
+
const icons = {}
|
|
219
|
+
for (const [size, file] of Object.entries(raw)) {
|
|
220
|
+
const numericSize = Number(size)
|
|
221
|
+
if (!Number.isInteger(numericSize) || numericSize <= 0) continue
|
|
222
|
+
if (typeof file !== 'string' || file.length === 0) continue
|
|
223
|
+
icons[numericSize] = file
|
|
224
|
+
}
|
|
225
|
+
return icons
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function normalizeLauncher(raw) {
|
|
229
|
+
if (!raw || typeof raw !== 'object') return { description: '', order: 0, accent: '', hidden: false }
|
|
230
|
+
return {
|
|
231
|
+
description: typeof raw.description === 'string' ? raw.description : '',
|
|
232
|
+
order: Number.isFinite(raw.order) ? raw.order : 0,
|
|
233
|
+
accent: typeof raw.accent === 'string' ? raw.accent : '',
|
|
234
|
+
hidden: raw.hidden === true
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function normalizeManifestRelativePath(value) {
|
|
239
|
+
const normalized = path.posix.normalize(String(value || '').replaceAll('\\', '/'))
|
|
240
|
+
if (!normalized || normalized === '.' || normalized === '..') return ''
|
|
241
|
+
if (path.posix.isAbsolute(normalized) || normalized.startsWith('../')) return ''
|
|
242
|
+
if (normalized.includes('/../') || normalized.includes(';')) return ''
|
|
243
|
+
return normalized
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function normalizeNativeSources(raw) {
|
|
247
|
+
if (!Array.isArray(raw)) return []
|
|
248
|
+
const sources = raw.map(normalizeManifestRelativePath).filter((source) => /\.(?:c|cc|cpp|cxx|m|mm|S)$/.test(source))
|
|
249
|
+
return [...new Set(sources)]
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function normalizeApp(root, packageJson) {
|
|
156
253
|
const gea = packageJson.gea || {}
|
|
254
|
+
const id = typeof gea.id === 'string' && gea.id ? gea.id : packageId(packageJson.name)
|
|
157
255
|
return {
|
|
158
|
-
id
|
|
159
|
-
name:
|
|
160
|
-
packageName: packageJson.name ||
|
|
161
|
-
version: packageJson.version || '',
|
|
256
|
+
id,
|
|
257
|
+
name: typeof gea.name === 'string' && gea.name ? gea.name : packageJson.name || id,
|
|
258
|
+
packageName: packageJson.name || id,
|
|
259
|
+
version: packageJson.version || '0.0.0',
|
|
162
260
|
root: path.resolve(root),
|
|
163
|
-
entry: gea.entry
|
|
164
|
-
runtime: gea.runtime
|
|
165
|
-
targets: gea.targets
|
|
166
|
-
icons: gea.icons
|
|
167
|
-
|
|
261
|
+
entry: typeof gea.entry === 'string' && gea.entry ? gea.entry : 'index.tsx',
|
|
262
|
+
runtime: typeof gea.runtime === 'string' && gea.runtime ? gea.runtime : 'gea',
|
|
263
|
+
targets: normalizeTargets(gea.targets),
|
|
264
|
+
icons: normalizeIcons(gea.icons),
|
|
265
|
+
nativeSources: normalizeNativeSources(gea.nativeSources),
|
|
266
|
+
launcher: normalizeLauncher(gea.launcher),
|
|
168
267
|
manifest: gea,
|
|
169
268
|
packageJson
|
|
170
269
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { resolvePicotoolSelection } from '../boards/usb.mjs'
|
|
7
|
+
import { CliError, ExitCode, fail } from '../errors.mjs'
|
|
8
|
+
import { appCmakeMeta } from '../manifest.mjs'
|
|
9
|
+
import { formatCommand } from '../run.mjs'
|
|
10
|
+
|
|
11
|
+
// Raspberry Pi RP2350 boards through the Pico SDK: cmake configure + build,
|
|
12
|
+
// then UF2 over the BOOTSEL volume or picotool.
|
|
13
|
+
|
|
14
|
+
const firmwareStems = {
|
|
15
|
+
'rp2350-waveshare-touch-amoled-2.41': 'gea_rp2350_touch_amoled_241',
|
|
16
|
+
'rp2350-tufty-2350': 'gea_rp2350_tufty_2350'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toolchainHasRuntime(root) {
|
|
20
|
+
if (!existsSync(path.join(root, 'bin', 'arm-none-eabi-gcc'))) return false
|
|
21
|
+
const stack = [root]
|
|
22
|
+
while (stack.length) {
|
|
23
|
+
const dir = stack.pop()
|
|
24
|
+
let entries = []
|
|
25
|
+
try {
|
|
26
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
27
|
+
} catch {
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (entry.isFile() && entry.name === 'nosys.specs') return true
|
|
32
|
+
if (entry.isDirectory()) stack.push(path.join(dir, entry.name))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function globDirs(pattern) {
|
|
39
|
+
const parent = path.dirname(pattern)
|
|
40
|
+
const prefix = path.basename(pattern).replace(/\*.*$/, '')
|
|
41
|
+
const suffix = pattern.includes('*') ? pattern.slice(pattern.indexOf('*') + 1) : ''
|
|
42
|
+
try {
|
|
43
|
+
return readdirSync(parent)
|
|
44
|
+
.filter((name) => name.startsWith(prefix))
|
|
45
|
+
.map((name) => path.join(parent, name, suffix))
|
|
46
|
+
.filter((dir) => existsSync(dir))
|
|
47
|
+
} catch {
|
|
48
|
+
return []
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function prepareArmToolchain(env, stderr = () => {}) {
|
|
53
|
+
if (env.PICO_TOOLCHAIN_PATH) {
|
|
54
|
+
return { ...env, PATH: `${path.join(env.PICO_TOOLCHAIN_PATH, 'bin')}${path.delimiter}${env.PATH || ''}` }
|
|
55
|
+
}
|
|
56
|
+
const probe = spawnSync('arm-none-eabi-gcc', ['-print-file-name=nosys.specs'], { env, encoding: 'utf8' })
|
|
57
|
+
const specs = (probe.stdout || '').trim()
|
|
58
|
+
if (probe.status === 0 && specs && specs !== 'nosys.specs' && existsSync(specs)) return env
|
|
59
|
+
const candidates = [
|
|
60
|
+
...globDirs(path.join(os.homedir(), 'Tools', 'arm-gnu-toolchain-*', 'extract', 'Payload')),
|
|
61
|
+
...globDirs('/Applications/ArmGNUToolchain/*/arm-none-eabi')
|
|
62
|
+
]
|
|
63
|
+
for (const candidate of candidates) {
|
|
64
|
+
if (toolchainHasRuntime(candidate)) {
|
|
65
|
+
stderr(`Using RP2350 ARM toolchain: ${candidate}`)
|
|
66
|
+
return { ...env, PICO_TOOLCHAIN_PATH: candidate, PATH: `${path.join(candidate, 'bin')}${path.delimiter}${env.PATH || ''}` }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
fail('RP2350 Pico SDK builds need an arm-none-eabi toolchain with newlib/nosys.specs.\nInstall the official Arm GNU Embedded toolchain or set PICO_TOOLCHAIN_PATH to its root.', ExitCode.missingDependency)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function cmakeBinary(env) {
|
|
73
|
+
if (env.CMAKE) return env.CMAKE
|
|
74
|
+
for (const dir of String(env.PATH || '').split(path.delimiter)) {
|
|
75
|
+
if (dir && existsSync(path.join(dir, 'cmake'))) return path.join(dir, 'cmake')
|
|
76
|
+
}
|
|
77
|
+
for (const candidate of ['/opt/homebrew/bin/cmake', '/usr/local/bin/cmake', '/Applications/CMake.app/Contents/bin/cmake']) {
|
|
78
|
+
if (existsSync(candidate)) return candidate
|
|
79
|
+
}
|
|
80
|
+
fail('CMake is required for RP2350 Pico SDK builds. Install cmake or set CMAKE=/path/to/cmake.', ExitCode.missingDependency)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function firmwareTarget(selection, app) {
|
|
84
|
+
const stem = firmwareStems[selection.target]
|
|
85
|
+
if (!stem) fail(`No RP2350 firmware target is registered for '${selection.target}'.`, ExitCode.usage)
|
|
86
|
+
return app ? `${stem}_app` : `${stem}_bringup`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function run(command, args, { cwd, env, dryRun, stdout, failureCode = ExitCode.buildFailed }) {
|
|
90
|
+
if (dryRun) {
|
|
91
|
+
stdout(formatCommand([command, ...args]))
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
const result = spawnSync(command, args, { cwd, env, stdio: 'inherit' })
|
|
95
|
+
if (result.error) throw result.error
|
|
96
|
+
if (result.status !== 0) throw new CliError(`ERROR: Command failed (${result.status ?? 1}): ${formatCommand([command, ...args])}`, failureCode)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function rp2350BuildDir(ctx, selection) {
|
|
100
|
+
return path.join(ctx.buildRoot, selection.target)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function buildRp2350({ ctx, selection, app = null, env, dryRun = false, stdout, stderr, configureOnly = false }) {
|
|
104
|
+
const toolEnv = prepareArmToolchain(env, stderr)
|
|
105
|
+
const cmake = cmakeBinary(toolEnv)
|
|
106
|
+
const buildDir = rp2350BuildDir(ctx, selection)
|
|
107
|
+
const configure = ['-S', selection.targetDir, '-B', buildDir]
|
|
108
|
+
const buildEnv = { ...toolEnv }
|
|
109
|
+
if (app) {
|
|
110
|
+
const meta = appCmakeMeta(ctx, app)
|
|
111
|
+
configure.push(`-DGEA_EMBEDDED_APP=${app.id}`, `-DGEA_EMBEDDED_APP_META=${meta}`)
|
|
112
|
+
buildEnv.GEA_EMBEDDED_APP = app.id
|
|
113
|
+
buildEnv.GEA_EMBEDDED_APP_META = meta
|
|
114
|
+
}
|
|
115
|
+
run(cmake, configure, { cwd: selection.targetDir, env: buildEnv, dryRun, stdout })
|
|
116
|
+
if (configureOnly) return { buildDir, uf2: '' }
|
|
117
|
+
const target = firmwareTarget(selection, app)
|
|
118
|
+
run(cmake, ['--build', buildDir, '--target', target], { cwd: selection.targetDir, env: buildEnv, dryRun, stdout })
|
|
119
|
+
return { buildDir, uf2: path.join(buildDir, `${target}.uf2`) }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function bootselVolumes(env) {
|
|
123
|
+
const user = env.USER || 'user'
|
|
124
|
+
return [
|
|
125
|
+
...globDirs('/Volumes/RPI-RP2*'),
|
|
126
|
+
...globDirs('/Volumes/RP2350*'),
|
|
127
|
+
`/media/${user}/RPI-RP2`,
|
|
128
|
+
`/media/${user}/RP2350`,
|
|
129
|
+
`/run/media/${user}/RPI-RP2`,
|
|
130
|
+
`/run/media/${user}/RP2350`
|
|
131
|
+
].filter((dir) => existsSync(dir))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function flashRp2350({ selection, uf2, env, dryRun = false, stdout }) {
|
|
135
|
+
if (!dryRun && !existsSync(uf2)) fail(`UF2 image not found after build: ${uf2}`, ExitCode.deployFailed)
|
|
136
|
+
const [volume] = bootselVolumes(env)
|
|
137
|
+
if (volume) {
|
|
138
|
+
const copyArgs = process.platform === 'darwin' ? ['-X', uf2, `${volume}/`] : [uf2, `${volume}/`]
|
|
139
|
+
run('cp', copyArgs, { env, dryRun, stdout, failureCode: ExitCode.deployFailed })
|
|
140
|
+
if (!dryRun) spawnSync('sync', [], { stdio: 'ignore' })
|
|
141
|
+
stdout(`Copied UF2 to ${volume}. The board should reboot automatically.`)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
const picotool = spawnSync('picotool', ['version'], { env, stdio: 'ignore' })
|
|
145
|
+
if (!picotool.error) {
|
|
146
|
+
const selectionArgs = selection.usbSerial ? resolvePicotoolSelection({ serial: selection.usbSerial }) : []
|
|
147
|
+
run('picotool', ['load', ...selectionArgs, '-f', '-x', uf2], { env, dryRun, stdout, failureCode: ExitCode.deployFailed })
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
if (dryRun) {
|
|
151
|
+
stdout(formatCommand(['picotool', 'load', '-f', '-x', uf2]))
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
fail('Could not flash RP2350 target.\nInstall picotool or put the board in BOOTSEL mode so RPI-RP2/RP2350 is mounted, then retry.', ExitCode.deployFailed)
|
|
155
|
+
}
|
package/src/setup-wizard.mjs
CHANGED
|
@@ -5,7 +5,6 @@ import path from 'node:path'
|
|
|
5
5
|
import { knownBoards } from './board-catalog.mjs'
|
|
6
6
|
import { configureChipSelection, loadChipCatalog, validateGpioAssignments } from './chips.mjs'
|
|
7
7
|
import { flag, option } from './args.mjs'
|
|
8
|
-
import { createChildEnv } from './context.mjs'
|
|
9
8
|
import { ExitCode, fail } from './errors.mjs'
|
|
10
9
|
import { exists, readJson, writeJson } from './fs-utils.mjs'
|
|
11
10
|
import { ask, choose, confirm, createPrompt } from './prompts.mjs'
|
|
@@ -374,18 +373,15 @@ async function maybeInitializeBoardTarget(ctx, parsed, io, boardSetup) {
|
|
|
374
373
|
io.stdout(`Board initialization skipped. Later: npx gea setup --board ${boardSetup.alias}`)
|
|
375
374
|
return 0
|
|
376
375
|
}
|
|
377
|
-
if (!
|
|
378
|
-
io.stdout(
|
|
376
|
+
if (!ctx.targetsRoot) {
|
|
377
|
+
io.stdout(`@geastack/targets is not installed. Later: npx gea setup --board ${boardSetup.alias}`)
|
|
379
378
|
return 0
|
|
380
379
|
}
|
|
381
380
|
io.stdout(`Initializing board target '${boardSetup.alias}'...`)
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
failureCode: ExitCode.buildFailed,
|
|
387
|
-
stdout: io.stdout
|
|
388
|
-
})
|
|
381
|
+
const { buildCommand } = await import('./commands/board.mjs')
|
|
382
|
+
const setupParsed = { ...parsed, options: { ...parsed.options, board: boardSetup.alias, 'configure-only': true } }
|
|
383
|
+
const reloaded = { ...ctx, boardsConfig: boardConfigPath(ctx, parsed) }
|
|
384
|
+
return buildCommand(reloaded, setupParsed, [], io)
|
|
389
385
|
}
|
|
390
386
|
|
|
391
387
|
async function maybeSetupEspIdf(ctx, parsed, io, prompt, { force = false } = {}) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
|
|
6
|
+
import { resolveUsbSerialPort } from '../boards/usb.mjs'
|
|
7
|
+
import { CliError, ExitCode, fail } from '../errors.mjs'
|
|
8
|
+
import { formatCommand } from '../run.mjs'
|
|
9
|
+
|
|
10
|
+
// The one per-target hook: a target directory may ship a board.mjs whose
|
|
11
|
+
// `commandFor({ action, app, port, serial, pedalRoot })` names the external
|
|
12
|
+
// command to run. taurus-s3 uses it to defer to the taurus-pedal firmware
|
|
13
|
+
// tree, which lives in its own repository.
|
|
14
|
+
|
|
15
|
+
export function pedalRootCandidates(ctx, env) {
|
|
16
|
+
if (env.TAURUS_PEDAL_ROOT) return [path.resolve(env.TAURUS_PEDAL_ROOT)]
|
|
17
|
+
const targetsRoot = ctx.targetsRoot || ctx.projectRoot
|
|
18
|
+
return [path.resolve(targetsRoot, '../taurus-pedal'), path.resolve(targetsRoot, '../../coyotiv/taurus-pedal')]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function runTargetHook({ ctx, selection, action, app = null, env, dryRun = false, stdout }) {
|
|
22
|
+
const hook = path.join(selection.targetDir, 'board.mjs')
|
|
23
|
+
if (!existsSync(hook)) fail(`Target '${selection.target}' has no board.mjs hook at ${hook}.`, ExitCode.missingDependency)
|
|
24
|
+
const module = await import(pathToFileURL(hook).href)
|
|
25
|
+
if (typeof module.commandFor !== 'function') fail(`${hook} does not export commandFor().`, ExitCode.missingDependency)
|
|
26
|
+
const candidates = pedalRootCandidates(ctx, env)
|
|
27
|
+
const pedalRoot = candidates.find((candidate) => existsSync(path.join(candidate, 'esp32-s3-a2-full/flash-s3.sh'))) || ''
|
|
28
|
+
let port = selection.port
|
|
29
|
+
if (!port && selection.usbSerial && action === 'flash' && !dryRun) {
|
|
30
|
+
try {
|
|
31
|
+
port = resolveUsbSerialPort({ serial: selection.usbSerial })
|
|
32
|
+
} catch {
|
|
33
|
+
port = ''
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
let command
|
|
37
|
+
try {
|
|
38
|
+
command = module.commandFor({ action, app: app?.id || '', port, serial: selection.usbSerial, pedalRoot: pedalRoot || candidates[0] })
|
|
39
|
+
} catch (error) {
|
|
40
|
+
fail(error.message, ExitCode.usage)
|
|
41
|
+
}
|
|
42
|
+
if (!command) return 0
|
|
43
|
+
if (dryRun) {
|
|
44
|
+
stdout(formatCommand([command.executable, ...command.args]))
|
|
45
|
+
return 0
|
|
46
|
+
}
|
|
47
|
+
if (!pedalRoot) fail('Set TAURUS_PEDAL_ROOT to the taurus-pedal checkout; firmware sources remain in that repository.', ExitCode.missingDependency)
|
|
48
|
+
const result = spawnSync(command.executable, command.args, { env, stdio: 'inherit' })
|
|
49
|
+
if (result.error) throw result.error
|
|
50
|
+
if (result.status !== 0) throw new CliError(`ERROR: Command failed (${result.status ?? 1}): ${formatCommand([command.executable, ...command.args])}`, ExitCode.deployFailed)
|
|
51
|
+
return 0
|
|
52
|
+
}
|