diff --git a/scripts/generate-architecture-hierarchy.js b/scripts/generate-architecture-hierarchy.js index f6078a7..5b1510d 100644 --- a/scripts/generate-architecture-hierarchy.js +++ b/scripts/generate-architecture-hierarchy.js @@ -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) diff --git a/scripts/generate-versions.js b/scripts/generate-versions.js index c349a2c..ae1ebdd 100644 --- a/scripts/generate-versions.js +++ b/scripts/generate-versions.js @@ -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)