refactor: fix output paths

This commit is contained in:
Ben Allfree
2025-12-06 17:15:17 -08:00
parent 3a543de36b
commit b7c5eaa8d0
2 changed files with 240 additions and 245 deletions

View File

@@ -1,20 +1,20 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import fs from "node:fs"
import path from "node:path"
import { fileURLToPath } from "node:url"
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const FIRMWARE_DIR = path.resolve(__dirname, '../vendor/firmware');
const VARIANTS_DIR = path.join(FIRMWARE_DIR, 'variants');
const OUTPUT_FILE = path.resolve(__dirname, '../src/constants/architecture-hierarchy.json');
const FIRMWARE_DIR = path.resolve(__dirname, "../vendor/firmware")
const VARIANTS_DIR = path.join(FIRMWARE_DIR, "variants")
const OUTPUT_FILE = path.resolve(__dirname, "../constants/architecture-hierarchy.json")
/**
* Normalize architecture/target name (remove hyphens and underscores)
* This ensures consistent format matching PlatformIO architecture names
*/
function normalizeName(name) {
return name.replace(/[-_]/g, '');
return name.replace(/[-_]/g, "")
}
/**
@@ -22,130 +22,125 @@ function normalizeName(name) {
*/
function parseIniFile(filePath) {
if (!fs.existsSync(filePath)) {
return null;
return null
}
const content = fs.readFileSync(filePath, 'utf-8');
const lines = content.split('\n');
const content = fs.readFileSync(filePath, "utf-8")
const lines = content.split("\n")
let currentSection = null;
const sections = {};
let currentSection = null
const sections = {}
for (const line of lines) {
const trimmed = line.trim();
const trimmed = line.trim()
// Skip comments and empty lines
if (!trimmed || trimmed.startsWith(';')) {
continue;
if (!trimmed || trimmed.startsWith(";")) {
continue
}
// Section header: [section_name]
const sectionMatch = trimmed.match(/^\[(.+)\]$/);
const sectionMatch = trimmed.match(/^\[(.+)\]$/)
if (sectionMatch) {
currentSection = sectionMatch[1];
sections[currentSection] = {};
continue;
currentSection = sectionMatch[1]
sections[currentSection] = {}
continue
}
// Key-value pairs
if (currentSection && trimmed.includes('=')) {
const [key, ...valueParts] = trimmed.split('=');
const value = valueParts.join('=').trim();
sections[currentSection][key.trim()] = value;
if (currentSection && trimmed.includes("=")) {
const [key, ...valueParts] = trimmed.split("=")
const value = valueParts.join("=").trim()
sections[currentSection][key.trim()] = value
}
}
return sections;
return sections
}
/**
* Find all ini files recursively
*/
function findAllIniFiles() {
const iniFiles = [];
const iniFiles = []
if (!fs.existsSync(VARIANTS_DIR)) {
console.error(`Variants directory not found: ${VARIANTS_DIR}`);
process.exit(1);
console.error(`Variants directory not found: ${VARIANTS_DIR}`)
process.exit(1)
}
function scanDirectory(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
if (entry.name.startsWith('.')) {
continue;
if (entry.name.startsWith(".")) {
continue
}
const fullPath = path.join(dir, entry.name);
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
scanDirectory(fullPath);
} else if (entry.isFile() && entry.name.endsWith('.ini')) {
iniFiles.push(fullPath);
scanDirectory(fullPath)
} else if (entry.isFile() && entry.name.endsWith(".ini")) {
iniFiles.push(fullPath)
}
}
}
scanDirectory(VARIANTS_DIR);
scanDirectory(VARIANTS_DIR)
// Also include the main platformio.ini
const mainIni = path.join(FIRMWARE_DIR, 'platformio.ini');
const mainIni = path.join(FIRMWARE_DIR, "platformio.ini")
if (fs.existsSync(mainIni)) {
iniFiles.push(mainIni);
iniFiles.push(mainIni)
}
return iniFiles;
return iniFiles
}
/**
* Extract architecture name from base section name
*/
function extractArchFromBaseSection(sectionName) {
const match = sectionName.match(/^([a-z0-9]+)_base$/);
const match = sectionName.match(/^([a-z0-9]+)_base$/)
if (!match) {
return null;
return null
}
const arch = match[1];
const arch = match[1]
// Filter out non-architecture bases (library/feature bases)
const nonArchitectureBases = [
'arduino',
'networking',
'radiolib',
'environmental',
'device-ui',
'native', // Ignore native architecture
];
"arduino",
"networking",
"radiolib",
"environmental",
"device-ui",
"native", // Ignore native architecture
]
if (nonArchitectureBases.includes(arch)) {
return null;
return null
}
// Filter out variant bases that extend other bases (not root architectures)
// These are board-specific or variant-specific bases, not architecture bases
// We'll detect this by checking if they extend another _base (handled in buildParentMapping)
// Filter out board-specific bases
const boardSpecificPatterns = [
/heltec/i,
/crowpanel/i,
/mesh_tab/i,
/muzi/i,
];
const boardSpecificPatterns = [/heltec/i, /crowpanel/i, /mesh_tab/i, /muzi/i]
for (const pattern of boardSpecificPatterns) {
if (pattern.test(arch)) {
return null;
return null
}
}
// Architecture names typically don't have underscores (except for numbers)
if (arch.includes('_') && !arch.match(/^[a-z]+\d+$/)) {
return null;
if (arch.includes("_") && !arch.match(/^[a-z]+\d+$/)) {
return null
}
return arch;
return arch
}
/**
@@ -154,284 +149,284 @@ function extractArchFromBaseSection(sectionName) {
*/
function getParentFromExtends(extendsValue) {
if (!extendsValue) {
return null;
return null
}
// Handle comma-separated extends (take the first one)
const firstExtends = extendsValue.split(',')[0].trim();
const firstExtends = extendsValue.split(",")[0].trim()
// Remove env: prefix if present
const cleaned = firstExtends.replace(/^env:/, '');
const cleaned = firstExtends.replace(/^env:/, "")
// Check if it's an architecture base (e.g., "esp32_base")
const archMatch = cleaned.match(/^([a-z0-9]+)_base$/);
const archMatch = cleaned.match(/^([a-z0-9]+)_base$/)
if (archMatch) {
const parent = archMatch[1];
if (parent === 'arduino') {
return null;
const parent = archMatch[1]
if (parent === "arduino") {
return null
}
return parent;
return parent
}
// Otherwise return as-is (could be a variant base or another target)
return cleaned;
return cleaned
}
/**
* Build flat parent mapping for all targets, variant bases, and architectures
*/
function buildParentMapping() {
const allIniFiles = findAllIniFiles();
const allIniFiles = findAllIniFiles()
// Track all parent relationships: child -> parent
const parentMap = {};
const parentMap = {}
// Track all entities we've seen (targets, variant bases, architectures)
const allEntities = new Set();
const allEntities = new Set()
// First pass: collect all [env:target] sections first (they take precedence)
const targetNames = new Set();
const targetNames = new Set()
for (const iniFile of allIniFiles) {
const sections = parseIniFile(iniFile);
const sections = parseIniFile(iniFile)
if (!sections) {
continue;
continue
}
for (const [sectionName] of Object.entries(sections)) {
const envMatch = sectionName.match(/^env:(.+)$/);
const envMatch = sectionName.match(/^env:(.+)$/)
if (envMatch) {
targetNames.add(envMatch[1]);
targetNames.add(envMatch[1])
}
}
}
// Second pass: collect all relationships
for (const iniFile of allIniFiles) {
const sections = parseIniFile(iniFile);
const sections = parseIniFile(iniFile)
if (!sections) {
continue;
continue
}
for (const [sectionName, sectionData] of Object.entries(sections)) {
const extendsValue = sectionData.extends;
const extendsValue = sectionData.extends
if (!extendsValue) {
continue;
continue
}
// Handle [env:target] sections
const envMatch = sectionName.match(/^env:(.+)$/);
const envMatch = sectionName.match(/^env:(.+)$/)
if (envMatch) {
const targetName = envMatch[1];
allEntities.add(targetName);
const parent = getParentFromExtends(extendsValue);
const targetName = envMatch[1]
allEntities.add(targetName)
const parent = getParentFromExtends(extendsValue)
if (parent) {
parentMap[targetName] = parent;
allEntities.add(parent);
parentMap[targetName] = parent
allEntities.add(parent)
}
continue;
continue
}
// Handle [variant_base] sections (variant-specific bases like heltec_v4_base)
const variantBaseMatch = sectionName.match(/^(.+)_base$/);
const variantBaseMatch = sectionName.match(/^(.+)_base$/)
if (variantBaseMatch) {
const variantBaseName = sectionName; // Keep full name like "heltec_v4_base"
allEntities.add(variantBaseName);
const parent = getParentFromExtends(extendsValue);
const variantBaseName = sectionName // Keep full name like "heltec_v4_base"
allEntities.add(variantBaseName)
const parent = getParentFromExtends(extendsValue)
if (parent) {
parentMap[variantBaseName] = parent;
allEntities.add(parent);
parentMap[variantBaseName] = parent
allEntities.add(parent)
}
continue;
continue
}
// Handle [arch_base] sections (architecture bases)
// Only treat as architecture base if it doesn't extend another _base (those are variant bases)
const arch = extractArchFromBaseSection(sectionName);
const arch = extractArchFromBaseSection(sectionName)
if (arch) {
// Check if this extends another _base - if so, it's a variant base, not an architecture base
const parentMatch = extendsValue.match(/([a-z0-9]+)_base/);
if (parentMatch && parentMatch[1] !== 'arduino') {
const parentMatch = extendsValue.match(/([a-z0-9]+)_base/)
if (parentMatch && parentMatch[1] !== "arduino") {
// This extends another _base, so it's a variant base, not an architecture base
// Treat it as a variant base instead
const variantBaseName = sectionName;
allEntities.add(variantBaseName);
const parent = getParentFromExtends(extendsValue);
const variantBaseName = sectionName
allEntities.add(variantBaseName)
const parent = getParentFromExtends(extendsValue)
if (parent) {
parentMap[variantBaseName] = parent;
allEntities.add(parent);
parentMap[variantBaseName] = parent
allEntities.add(parent)
}
continue;
continue
}
// This is a true architecture base
const archBaseName = `${arch}_base`;
allEntities.add(archBaseName);
const parent = getParentFromExtends(extendsValue);
const archBaseName = `${arch}_base`
allEntities.add(archBaseName)
const parent = getParentFromExtends(extendsValue)
if (parent) {
parentMap[archBaseName] = parent;
allEntities.add(parent);
parentMap[archBaseName] = parent
allEntities.add(parent)
}
// Also add the architecture itself (without _base suffix)
// Only if it doesn't conflict with an existing target name
if (!targetNames.has(arch)) {
allEntities.add(arch);
parentMap[arch] = archBaseName;
allEntities.add(arch)
parentMap[arch] = archBaseName
}
continue;
continue
}
}
}
// Second pass: resolve architecture base names to architecture names
// Build a mapping of architecture bases to their parent architectures
const archBaseToArch = {};
const archToParentArch = {};
const archBaseToArch = {}
const archToParentArch = {}
// First, map architecture bases to their parent architectures
for (const [child, parent] of Object.entries(parentMap)) {
if (child.endsWith('_base')) {
const archMatch = child.match(/^([a-z0-9]+)_base$/);
if (child.endsWith("_base")) {
const archMatch = child.match(/^([a-z0-9]+)_base$/)
if (archMatch) {
const arch = archMatch[1];
const isArchitecture = extractArchFromBaseSection(child) !== null;
const arch = archMatch[1]
const isArchitecture = extractArchFromBaseSection(child) !== null
if (isArchitecture) {
archBaseToArch[child] = arch;
archBaseToArch[child] = arch
// The parent of an architecture base is the parent architecture
if (parent) {
archToParentArch[arch] = parent;
archToParentArch[arch] = parent
}
}
}
}
}
// Third pass: resolve all parent references
// Replace architecture base references with architecture references
const resolvedParentMap = {};
const resolvedParentMap = {}
for (const [child, parent] of Object.entries(parentMap)) {
if (!parent) {
resolvedParentMap[child] = null;
continue;
resolvedParentMap[child] = null
continue
}
// If parent is an architecture base, resolve to architecture
if (archBaseToArch[parent]) {
resolvedParentMap[child] = archBaseToArch[parent];
resolvedParentMap[child] = archBaseToArch[parent]
} else {
resolvedParentMap[child] = parent;
resolvedParentMap[child] = parent
}
}
// Add architecture entries themselves (pointing to their parent architecture)
for (const [arch, parentArch] of Object.entries(archToParentArch)) {
if (!resolvedParentMap[arch]) {
resolvedParentMap[arch] = parentArch;
resolvedParentMap[arch] = parentArch
}
}
// Mark base architectures (those with no parent) as null
const baseArchitectures = ['esp32', 'nrf52', 'rp2040', 'rp2350', 'stm32', 'portduino'];
const baseArchitectures = ["esp32", "nrf52", "rp2040", "rp2350", "stm32", "portduino"]
for (const baseArch of baseArchitectures) {
if (allEntities.has(baseArch)) {
// If it doesn't have a parent or parent resolves to null, it's a base
if (!resolvedParentMap[baseArch]) {
resolvedParentMap[baseArch] = null;
resolvedParentMap[baseArch] = null
}
}
}
// Remove native entries (ignore native architecture)
const keysToRemove = Object.keys(resolvedParentMap).filter(k => k === 'native' || k.startsWith('native'));
const keysToRemove = Object.keys(resolvedParentMap).filter(k => k === "native" || k.startsWith("native"))
for (const key of keysToRemove) {
delete resolvedParentMap[key];
delete resolvedParentMap[key]
}
// Normalize all keys and values (strip hyphens and underscores)
const normalizedMap = {};
const normalizedMap = {}
for (const [key, value] of Object.entries(resolvedParentMap)) {
const normalizedKey = normalizeName(key);
const normalizedValue = value !== null ? normalizeName(value) : null;
normalizedMap[normalizedKey] = normalizedValue;
const normalizedKey = normalizeName(key)
const normalizedValue = value !== null ? normalizeName(value) : null
normalizedMap[normalizedKey] = normalizedValue
}
return normalizedMap;
return normalizedMap
}
/**
* Validate parent mapping for conflicts and issues
*/
function validateParentMapping(parentMap) {
const errors = [];
const warnings = [];
const errors = []
const warnings = []
// Check for duplicate keys (shouldn't happen, but verify)
const keys = Object.keys(parentMap);
const keySet = new Set(keys);
const keys = Object.keys(parentMap)
const keySet = new Set(keys)
if (keys.length !== keySet.size) {
errors.push('Duplicate keys found in mapping');
errors.push("Duplicate keys found in mapping")
}
// Check for self-references (child pointing to itself)
for (const [child, parent] of Object.entries(parentMap)) {
if (parent === child) {
errors.push(`Self-reference detected: "${child}" points to itself`);
errors.push(`Self-reference detected: "${child}" points to itself`)
}
}
// Check for circular references
for (const key of keys) {
const visited = new Set();
let current = key;
let depth = 0;
const maxDepth = 100; // Safety limit
const visited = new Set()
let current = key
let depth = 0
const maxDepth = 100 // Safety limit
while (current && parentMap[current] !== null && parentMap[current] !== undefined) {
if (visited.has(current)) {
errors.push(`Circular reference detected involving "${current}"`);
break;
errors.push(`Circular reference detected involving "${current}"`)
break
}
visited.add(current);
current = parentMap[current];
depth++;
visited.add(current)
current = parentMap[current]
depth++
if (depth > maxDepth) {
errors.push(`Infinite loop detected starting from "${key}"`);
break;
errors.push(`Infinite loop detected starting from "${key}"`)
break
}
}
}
// Check for missing parent references
for (const [child, parent] of Object.entries(parentMap)) {
if (parent !== null && parent !== undefined && !parentMap.hasOwnProperty(parent)) {
warnings.push(`"${child}" references missing parent "${parent}"`);
warnings.push(`"${child}" references missing parent "${parent}"`)
}
}
if (errors.length > 0) {
console.error('\nVALIDATION ERRORS:');
errors.forEach(err => console.error(` ERROR: ${err}`));
process.exit(1);
console.error("\nVALIDATION ERRORS:")
errors.forEach(err => console.error(` ERROR: ${err}`))
process.exit(1)
}
if (warnings.length > 0) {
console.warn('\nVALIDATION WARNINGS:');
warnings.forEach(warn => console.warn(` WARNING: ${warn}`));
console.warn("\nVALIDATION WARNINGS:")
warnings.forEach(warn => console.warn(` WARNING: ${warn}`))
}
console.log(`✓ Validation passed: ${keys.length} entries, no conflicts`);
console.log(`✓ Validation passed: ${keys.length} entries, no conflicts`)
}
/**
* Generate JSON file
*/
function generateFile(parentMap) {
validateParentMapping(parentMap);
const content = JSON.stringify(parentMap, null, 2);
fs.writeFileSync(OUTPUT_FILE, content);
const count = Object.keys(parentMap).length;
console.log(`Generated ${OUTPUT_FILE} with ${count} entries`);
validateParentMapping(parentMap)
const content = JSON.stringify(parentMap, null, 2)
fs.writeFileSync(OUTPUT_FILE, content)
const count = Object.keys(parentMap).length
console.log(`Generated ${OUTPUT_FILE} with ${count} entries`)
}
const parentMap = buildParentMapping();
generateFile(parentMap);
const parentMap = buildParentMapping()
generateFile(parentMap)

View File

@@ -1,49 +1,49 @@
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from "node:child_process"
import fs from "node:fs"
import path from "node:path"
import { fileURLToPath } from "node:url"
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const FIRMWARE_DIR = path.resolve(__dirname, '../vendor/firmware');
const OUTPUT_FILE = path.resolve(__dirname, '../src/constants/versions.ts');
const FIRMWARE_DIR = path.resolve(__dirname, "../vendor/firmware")
const OUTPUT_FILE = path.resolve(__dirname, "../constants/versions.ts")
function getVersions() {
try {
// Fetch tags from the meshtastic remote
execSync('git fetch meshtastic --tags', { cwd: FIRMWARE_DIR, encoding: 'utf-8' });
// Run git tag in the firmware directory
const output = execSync('git tag', { cwd: FIRMWARE_DIR, encoding: 'utf-8' });
// Filter and sort tags
// We are looking for tags like v2.5.0...
const tags = output
.split('\n')
.map(tag => tag.trim())
.filter(tag => tag.startsWith('v') && tag.includes('.'))
// Simple sort for now, ideally semver sort but string sort works okay for fixed format vX.Y.Z
// We want reverse sort to show latest first
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: 'base' }));
try {
// Fetch tags from the meshtastic remote
execSync("git fetch meshtastic --tags", { cwd: FIRMWARE_DIR, encoding: "utf-8" })
return tags;
} catch (error) {
console.error('Error getting git tags:', error);
return [];
}
// Run git tag in the firmware directory
const output = execSync("git tag", { cwd: FIRMWARE_DIR, encoding: "utf-8" })
// Filter and sort tags
// We are looking for tags like v2.5.0...
const tags = output
.split("\n")
.map(tag => tag.trim())
.filter(tag => tag.startsWith("v") && tag.includes("."))
// Simple sort for now, ideally semver sort but string sort works okay for fixed format vX.Y.Z
// We want reverse sort to show latest first
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" }))
return tags
} catch (error) {
console.error("Error getting git tags:", error)
return []
}
}
function generateFile(versions) {
const content = `// This file is auto-generated by scripts/generate-versions.js
export const VERSIONS = ${JSON.stringify(versions, null, '\t')} as const;
const content = `// This file is auto-generated by scripts/generate-versions.js
export const VERSIONS = ${JSON.stringify(versions, null, "\t")} as const;
export type FirmwareVersion = typeof VERSIONS[number];
`;
`
fs.writeFileSync(OUTPUT_FILE, content);
console.log(`Generated ${OUTPUT_FILE} with ${versions.length} versions`);
fs.writeFileSync(OUTPUT_FILE, content)
console.log(`Generated ${OUTPUT_FILE} with ${versions.length} versions`)
}
const versions = getVersions();
generateFile(versions);
const versions = getVersions()
generateFile(versions)