mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-07-05 01:12:14 +02:00
438 lines
13 KiB
JavaScript
438 lines
13 KiB
JavaScript
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 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');
|
|
|
|
/**
|
|
* Normalize architecture/target name (remove hyphens and underscores)
|
|
* This ensures consistent format matching PlatformIO architecture names
|
|
*/
|
|
function normalizeName(name) {
|
|
return name.replace(/[-_]/g, '');
|
|
}
|
|
|
|
/**
|
|
* Parse PlatformIO ini file to extract sections and their properties
|
|
*/
|
|
function parseIniFile(filePath) {
|
|
if (!fs.existsSync(filePath)) {
|
|
return null;
|
|
}
|
|
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const lines = content.split('\n');
|
|
|
|
let currentSection = null;
|
|
const sections = {};
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
|
|
// Skip comments and empty lines
|
|
if (!trimmed || trimmed.startsWith(';')) {
|
|
continue;
|
|
}
|
|
|
|
// Section header: [section_name]
|
|
const sectionMatch = trimmed.match(/^\[(.+)\]$/);
|
|
if (sectionMatch) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
return sections;
|
|
}
|
|
|
|
/**
|
|
* Find all ini files recursively
|
|
*/
|
|
function findAllIniFiles() {
|
|
const iniFiles = [];
|
|
|
|
if (!fs.existsSync(VARIANTS_DIR)) {
|
|
console.error(`Variants directory not found: ${VARIANTS_DIR}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function scanDirectory(dir) {
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
if (entry.name.startsWith('.')) {
|
|
continue;
|
|
}
|
|
|
|
const fullPath = path.join(dir, entry.name);
|
|
|
|
if (entry.isDirectory()) {
|
|
scanDirectory(fullPath);
|
|
} else if (entry.isFile() && entry.name.endsWith('.ini')) {
|
|
iniFiles.push(fullPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
scanDirectory(VARIANTS_DIR);
|
|
// Also include the main platformio.ini
|
|
const mainIni = path.join(FIRMWARE_DIR, 'platformio.ini');
|
|
if (fs.existsSync(mainIni)) {
|
|
iniFiles.push(mainIni);
|
|
}
|
|
|
|
return iniFiles;
|
|
}
|
|
|
|
/**
|
|
* Extract architecture name from base section name
|
|
*/
|
|
function extractArchFromBaseSection(sectionName) {
|
|
const match = sectionName.match(/^([a-z0-9]+)_base$/);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
const arch = match[1];
|
|
|
|
// Filter out non-architecture bases (library/feature bases)
|
|
const nonArchitectureBases = [
|
|
'arduino',
|
|
'networking',
|
|
'radiolib',
|
|
'environmental',
|
|
'device-ui',
|
|
'native', // Ignore native architecture
|
|
];
|
|
|
|
if (nonArchitectureBases.includes(arch)) {
|
|
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,
|
|
];
|
|
|
|
for (const pattern of boardSpecificPatterns) {
|
|
if (pattern.test(arch)) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Architecture names typically don't have underscores (except for numbers)
|
|
if (arch.includes('_') && !arch.match(/^[a-z]+\d+$/)) {
|
|
return null;
|
|
}
|
|
|
|
return arch;
|
|
}
|
|
|
|
/**
|
|
* Extract parent from extends directive
|
|
* PlatformIO allows multiple extends separated by commas - take the first one
|
|
*/
|
|
function getParentFromExtends(extendsValue) {
|
|
if (!extendsValue) {
|
|
return null;
|
|
}
|
|
|
|
// Handle comma-separated extends (take the first one)
|
|
const firstExtends = extendsValue.split(',')[0].trim();
|
|
|
|
// Remove env: prefix if present
|
|
const cleaned = firstExtends.replace(/^env:/, '');
|
|
|
|
// Check if it's an architecture base (e.g., "esp32_base")
|
|
const archMatch = cleaned.match(/^([a-z0-9]+)_base$/);
|
|
if (archMatch) {
|
|
const parent = archMatch[1];
|
|
if (parent === 'arduino') {
|
|
return null;
|
|
}
|
|
return parent;
|
|
}
|
|
|
|
// Otherwise return as-is (could be a variant base or another target)
|
|
return cleaned;
|
|
}
|
|
|
|
/**
|
|
* Build flat parent mapping for all targets, variant bases, and architectures
|
|
*/
|
|
function buildParentMapping() {
|
|
const allIniFiles = findAllIniFiles();
|
|
|
|
// Track all parent relationships: child -> parent
|
|
const parentMap = {};
|
|
|
|
// Track all entities we've seen (targets, variant bases, architectures)
|
|
const allEntities = new Set();
|
|
|
|
// First pass: collect all [env:target] sections first (they take precedence)
|
|
const targetNames = new Set();
|
|
for (const iniFile of allIniFiles) {
|
|
const sections = parseIniFile(iniFile);
|
|
if (!sections) {
|
|
continue;
|
|
}
|
|
|
|
for (const [sectionName] of Object.entries(sections)) {
|
|
const envMatch = sectionName.match(/^env:(.+)$/);
|
|
if (envMatch) {
|
|
targetNames.add(envMatch[1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Second pass: collect all relationships
|
|
for (const iniFile of allIniFiles) {
|
|
const sections = parseIniFile(iniFile);
|
|
if (!sections) {
|
|
continue;
|
|
}
|
|
|
|
for (const [sectionName, sectionData] of Object.entries(sections)) {
|
|
const extendsValue = sectionData.extends;
|
|
if (!extendsValue) {
|
|
continue;
|
|
}
|
|
|
|
// Handle [env:target] sections
|
|
const envMatch = sectionName.match(/^env:(.+)$/);
|
|
if (envMatch) {
|
|
const targetName = envMatch[1];
|
|
allEntities.add(targetName);
|
|
const parent = getParentFromExtends(extendsValue);
|
|
if (parent) {
|
|
parentMap[targetName] = parent;
|
|
allEntities.add(parent);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Handle [variant_base] sections (variant-specific bases like heltec_v4_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);
|
|
if (parent) {
|
|
parentMap[variantBaseName] = parent;
|
|
allEntities.add(parent);
|
|
}
|
|
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);
|
|
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') {
|
|
// 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);
|
|
if (parent) {
|
|
parentMap[variantBaseName] = parent;
|
|
allEntities.add(parent);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// This is a true architecture base
|
|
const archBaseName = `${arch}_base`;
|
|
allEntities.add(archBaseName);
|
|
const parent = getParentFromExtends(extendsValue);
|
|
if (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;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Second pass: resolve architecture base names to architecture names
|
|
// Build a mapping of architecture bases to their parent architectures
|
|
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 (archMatch) {
|
|
const arch = archMatch[1];
|
|
const isArchitecture = extractArchFromBaseSection(child) !== null;
|
|
if (isArchitecture) {
|
|
archBaseToArch[child] = arch;
|
|
// The parent of an architecture base is the parent architecture
|
|
if (parent) {
|
|
archToParentArch[arch] = parent;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Third pass: resolve all parent references
|
|
// Replace architecture base references with architecture references
|
|
const resolvedParentMap = {};
|
|
|
|
for (const [child, parent] of Object.entries(parentMap)) {
|
|
if (!parent) {
|
|
resolvedParentMap[child] = null;
|
|
continue;
|
|
}
|
|
|
|
// If parent is an architecture base, resolve to architecture
|
|
if (archBaseToArch[parent]) {
|
|
resolvedParentMap[child] = archBaseToArch[parent];
|
|
} else {
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Mark base architectures (those with no parent) as null
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove native entries (ignore native architecture)
|
|
const keysToRemove = Object.keys(resolvedParentMap).filter(k => k === 'native' || k.startsWith('native'));
|
|
for (const key of keysToRemove) {
|
|
delete resolvedParentMap[key];
|
|
}
|
|
|
|
// Normalize all keys and values (strip hyphens and underscores)
|
|
const normalizedMap = {};
|
|
for (const [key, value] of Object.entries(resolvedParentMap)) {
|
|
const normalizedKey = normalizeName(key);
|
|
const normalizedValue = value !== null ? normalizeName(value) : null;
|
|
normalizedMap[normalizedKey] = normalizedValue;
|
|
}
|
|
|
|
return normalizedMap;
|
|
}
|
|
|
|
/**
|
|
* Validate parent mapping for conflicts and issues
|
|
*/
|
|
function validateParentMapping(parentMap) {
|
|
const errors = [];
|
|
const warnings = [];
|
|
|
|
// Check for duplicate keys (shouldn't happen, but verify)
|
|
const keys = Object.keys(parentMap);
|
|
const keySet = new Set(keys);
|
|
if (keys.length !== keySet.size) {
|
|
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`);
|
|
}
|
|
}
|
|
|
|
// Check for circular references
|
|
for (const key of keys) {
|
|
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;
|
|
}
|
|
visited.add(current);
|
|
current = parentMap[current];
|
|
depth++;
|
|
if (depth > maxDepth) {
|
|
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}"`);
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
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.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`);
|
|
}
|
|
|
|
const parentMap = buildParentMapping();
|
|
generateFile(parentMap);
|