Implement build graph structure and dependency resolution for shared libraries and tools

main
Joca 2025-11-14 16:50:25 -03:00
parent b2cdeaebd6
commit 1ab3f9c175
Signed by: jocadbz
GPG Key ID: B1836DCE2F50BDF7
1 changed files with 434 additions and 222 deletions

View File

@ -38,11 +38,39 @@ struct CompileResult {
err string err string
} }
struct BuildNode {
id string
name string
target BuildTarget
raw_dependencies []string
mut:
dependencies []string
shared_lib_idx int = -1
tool_idx int = -1
is_directive bool
directive config.BuildDirective
output_path string
}
struct BuildGraph {
nodes []BuildNode
node_index map[string]int
order []int
unresolved map[string][]string
}
struct DirectiveBuildContext {
source string
object string
target_config config.TargetConfig
}
const ansi_reset = '\x1b[0m' const ansi_reset = '\x1b[0m'
const ansi_red = '\x1b[31m' const ansi_red = '\x1b[31m'
const ansi_green = '\x1b[32m' const ansi_green = '\x1b[32m'
const ansi_yellow = '\x1b[33m' const ansi_yellow = '\x1b[33m'
const ansi_cyan = '\x1b[36m' const ansi_cyan = '\x1b[36m'
const skip_directive_err = 'skip_directive'
fn should_use_color() bool { fn should_use_color() bool {
return os.getenv('NO_COLOR') == '' && os.is_atty(1) != 0 return os.getenv('NO_COLOR') == '' && os.is_atty(1) != 0
@ -55,6 +83,89 @@ fn colorize(text string, color string) string {
return '${color}${text}${ansi_reset}' return '${color}${text}${ansi_reset}'
} }
fn register_alias(mut alias_map map[string]string, alias string, id string) {
trimmed := alias.trim_space()
if trimmed == '' {
return
}
if trimmed in alias_map {
return
}
alias_map[trimmed] = id
}
fn resolve_dependency(alias_map map[string]string, dep string) string {
trimmed := dep.trim_space()
if trimmed == '' {
return ''
}
mut candidates := []string{}
candidates << trimmed
if trimmed.ends_with('.so') && trimmed.len > 3 {
base := trimmed[..trimmed.len - 3]
candidates << base
if base.starts_with('lib/') && base.len > 4 {
candidates << base[4..]
}
}
if trimmed.starts_with('lib/') && trimmed.len > 4 {
candidates << trimmed[4..]
}
if trimmed.contains('/') {
parts := trimmed.split('/')
if parts.len > 0 {
candidates << parts[parts.len - 1]
}
}
for candidate in candidates {
if candidate in alias_map {
return alias_map[candidate]
}
}
return ''
}
fn topo_sort_nodes(nodes []BuildNode, node_index map[string]int) ![]int {
mut indegree := []int{len: nodes.len, init: 0}
mut adjacency := [][]int{len: nodes.len}
for idx, node in nodes {
for dep_id in node.dependencies {
dep_idx := node_index[dep_id] or {
return error('Unknown dependency ${dep_id} referenced by node ${node.id}')
}
adjacency[dep_idx] << idx
indegree[idx] += 1
}
}
mut queue := []int{}
for idx, deg in indegree {
if deg == 0 {
queue << idx
}
}
mut order := []int{}
mut head := 0
for head < queue.len {
current := queue[head]
head += 1
order << current
for neighbor in adjacency[current] {
indegree[neighbor] -= 1
if indegree[neighbor] == 0 {
queue << neighbor
}
}
}
if order.len != nodes.len {
return error('Build graph contains a cycle or unresolved dependency')
}
return order
}
fn run_compile_tasks(tasks []CompileTask, build_config config.BuildConfig) ![]string { fn run_compile_tasks(tasks []CompileTask, build_config config.BuildConfig) ![]string {
mut object_files := []string{} mut object_files := []string{}
if tasks.len == 0 { if tasks.len == 0 {
@ -124,6 +235,327 @@ fn run_compile_tasks(tasks []CompileTask, build_config config.BuildConfig) ![]st
return object_files return object_files
} }
fn plan_build_graph(build_config &config.BuildConfig) !BuildGraph {
mut nodes := []BuildNode{}
mut alias_map := map[string]string{}
for idx, lib_config in build_config.shared_libs {
if lib_config.sources.len == 0 {
if build_config.debug || build_config.verbose || lib_config.debug || lib_config.verbose {
println('Skipping empty shared library: ${lib_config.name}')
}
continue
}
node_id := 'shared:${lib_config.name}'
node := BuildNode{
id: node_id
name: lib_config.name
target: BuildTarget.shared_lib
raw_dependencies: lib_config.libraries.clone()
shared_lib_idx: idx
}
nodes << node
register_alias(mut alias_map, lib_config.name, node_id)
register_alias(mut alias_map, 'lib/${lib_config.name}', node_id)
register_alias(mut alias_map, '${lib_config.name}.so', node_id)
register_alias(mut alias_map, 'lib/${lib_config.name}.so', node_id)
}
for directive in build_config.build_directives {
node_id := 'directive:${directive.unit_name}'
node := BuildNode{
id: node_id
name: directive.unit_name
target: if directive.is_shared { BuildTarget.shared_lib } else { BuildTarget.tool }
raw_dependencies: directive.depends_units.clone()
is_directive: true
directive: directive
output_path: directive.output_path
}
nodes << node
register_alias(mut alias_map, directive.unit_name, node_id)
parts := directive.unit_name.split('/')
if parts.len > 0 {
base := parts[parts.len - 1]
register_alias(mut alias_map, base, node_id)
if directive.is_shared {
register_alias(mut alias_map, '${base}.so', node_id)
}
}
if directive.output_path != '' {
register_alias(mut alias_map, directive.output_path, node_id)
}
}
for idx, tool_config in build_config.tools {
if tool_config.sources.len == 0 {
if build_config.debug || build_config.verbose || tool_config.debug || tool_config.verbose {
println('Skipping empty tool: ${tool_config.name}')
}
continue
}
node_id := 'tool:${tool_config.name}'
node := BuildNode{
id: node_id
name: tool_config.name
target: BuildTarget.tool
raw_dependencies: tool_config.libraries.clone()
tool_idx: idx
}
nodes << node
register_alias(mut alias_map, tool_config.name, node_id)
register_alias(mut alias_map, 'tools/${tool_config.name}', node_id)
}
mut node_index := map[string]int{}
for idx, node in nodes {
if node.id in node_index {
return error('Duplicate node id detected in build graph: ${node.id}')
}
node_index[node.id] = idx
}
mut unresolved := map[string][]string{}
for idx in 0 .. nodes.len {
mut resolved := []string{}
mut missing := []string{}
for dep in nodes[idx].raw_dependencies {
dep_id := resolve_dependency(alias_map, dep)
if dep_id == '' {
missing << dep
continue
}
if dep_id !in resolved {
resolved << dep_id
}
}
nodes[idx].dependencies = resolved
if missing.len > 0 {
unresolved[nodes[idx].id] = missing
}
}
order := if nodes.len > 0 { topo_sort_nodes(nodes, node_index)! } else { []int{} }
return BuildGraph{
nodes: nodes
node_index: node_index
order: order
unresolved: unresolved
}
}
fn execute_build_graph(mut build_config config.BuildConfig, graph BuildGraph) ! {
for node_idx in graph.order {
node := graph.nodes[node_idx]
if node.id in graph.unresolved && build_config.verbose {
missing := graph.unresolved[node.id]
println(colorize('Warning: Unresolved dependencies for ${node.name}: ${missing.join(", ")}', ansi_yellow))
}
if node.is_directive && (build_config.debug || build_config.verbose) {
println('Building unit: ${node.name}')
}
match node.target {
.shared_lib {
if node.is_directive {
build_directive_shared(node.directive, build_config) or {
return error('Failed to build shared directive ${node.name}: ${err}')
}
} else if node.shared_lib_idx >= 0 {
mut lib_config := &build_config.shared_libs[node.shared_lib_idx]
if build_config.debug || build_config.verbose || lib_config.debug || lib_config.verbose {
println('Building shared library: ${lib_config.name}')
}
build_shared_library(mut lib_config, build_config) or {
return error('Failed to build shared library ${lib_config.name}: ${err}')
}
if build_config.verbose {
println('Built shared library: ${lib_config.name}')
}
}
}
.tool {
if node.is_directive {
build_directive_tool(node.directive, build_config) or {
return error('Failed to build tool directive ${node.name}: ${err}')
}
} else if node.tool_idx >= 0 {
mut tool_config := &build_config.tools[node.tool_idx]
if build_config.debug || build_config.verbose || tool_config.debug || tool_config.verbose {
println('Building tool: ${tool_config.name}')
}
build_tool(mut tool_config, build_config) or {
return error('Failed to build tool ${tool_config.name}: ${err}')
}
if build_config.verbose {
println('Built tool: ${tool_config.name}')
}
}
}
}
}
}
fn resolve_directive_source(directive config.BuildDirective, build_config config.BuildConfig) string {
extensions := ['.cpp', '.cc', '.cxx']
for ext in extensions {
candidate := os.join_path(build_config.src_dir, directive.unit_name + ext)
if os.is_file(candidate) {
return candidate
}
}
parts := directive.unit_name.split('/')
base := if parts.len > 0 { parts[parts.len - 1] } else { directive.unit_name }
for ext in extensions {
candidate := os.join_path(build_config.src_dir, base + ext)
if os.is_file(candidate) {
return candidate
}
}
return ''
}
fn prepare_directive_build(directive config.BuildDirective, build_config config.BuildConfig) !DirectiveBuildContext {
source_file := resolve_directive_source(directive, build_config)
if source_file == '' {
if build_config.verbose {
println(colorize('Warning: Source file not found for unit ${directive.unit_name}', ansi_yellow))
}
return error(skip_directive_err)
}
object_dir := os.join_path(build_config.build_dir, directive.unit_name)
os.mkdir_all(object_dir) or {
return error('Failed to create object directory: ${object_dir}')
}
obj_file := get_object_file(source_file, object_dir)
obj_path := os.dir(obj_file)
os.mkdir_all(obj_path) or {
return error('Failed to create object directory: ${obj_path}')
}
target_config := if directive.is_shared {
config.TargetConfig(config.SharedLibConfig{
name: directive.unit_name
sources: [source_file]
libraries: directive.link_libs
cflags: directive.cflags
ldflags: directive.ldflags
debug: build_config.debug
optimize: build_config.optimize
verbose: build_config.verbose
})
} else {
config.TargetConfig(config.ToolConfig{
name: directive.unit_name
sources: [source_file]
libraries: directive.link_libs
cflags: directive.cflags
ldflags: directive.ldflags
debug: build_config.debug
optimize: build_config.optimize
verbose: build_config.verbose
})
}
return DirectiveBuildContext{
source: source_file
object: obj_file
target_config: target_config
}
}
fn build_directive_shared(directive config.BuildDirective, build_config config.BuildConfig) ! {
ctx := prepare_directive_build(directive, build_config) or {
if err.msg() == skip_directive_err {
return
}
return error(err.msg())
}
if needs_recompile(ctx.source, ctx.object) {
if build_config.debug || build_config.verbose {
println('Compiling ${directive.unit_name}: ${ctx.source}...')
}
compile_file(ctx.source, ctx.object, build_config, ctx.target_config) or {
return error('Failed to compile ${ctx.source} for ${directive.unit_name}')
}
} else if build_config.verbose {
println('Using cached ${ctx.object} for ${directive.unit_name}')
}
lib_output_dir := os.join_path(build_config.bin_dir, 'lib')
os.mkdir_all(lib_output_dir) or {
return error('Failed to create shared lib output directory: ${lib_output_dir}')
}
if build_config.debug || build_config.verbose {
parts := directive.unit_name.split('/')
base := if parts.len > 0 { parts[parts.len - 1] } else { directive.unit_name }
println('Linking shared library: ${lib_output_dir}/${base}.so')
}
link_shared_library([ctx.object], directive.unit_name, lib_output_dir, build_config, config.SharedLibConfig{
name: directive.unit_name
libraries: directive.link_libs
debug: build_config.debug
optimize: build_config.optimize
verbose: build_config.verbose
ldflags: directive.ldflags
}) or {
return error('Failed to link shared library ${directive.unit_name}')
}
if build_config.verbose {
println('Successfully built unit: ${directive.unit_name}')
}
}
fn build_directive_tool(directive config.BuildDirective, build_config config.BuildConfig) ! {
ctx := prepare_directive_build(directive, build_config) or {
if err.msg() == skip_directive_err {
return
}
return error(err.msg())
}
if needs_recompile(ctx.source, ctx.object) {
if build_config.debug || build_config.verbose {
println('Compiling ${directive.unit_name}: ${ctx.source}...')
}
compile_file(ctx.source, ctx.object, build_config, ctx.target_config) or {
return error('Failed to compile ${ctx.source} for ${directive.unit_name}')
}
} else if build_config.verbose {
println('Using cached ${ctx.object} for ${directive.unit_name}')
}
executable := os.join_path(build_config.bin_dir, directive.output_path)
if build_config.debug || build_config.verbose {
println('Linking executable: ${executable}')
}
link_tool([ctx.object], executable, build_config, config.ToolConfig{
name: directive.unit_name
libraries: directive.link_libs
debug: build_config.debug
optimize: build_config.optimize
verbose: build_config.verbose
ldflags: directive.ldflags
}) or {
return error('Failed to link executable ${directive.unit_name}')
}
if build_config.verbose {
println('Successfully built unit: ${directive.unit_name}')
}
}
pub fn build(mut build_config config.BuildConfig) ! { pub fn build(mut build_config config.BuildConfig) ! {
// Run build flow and ensure that if any error occurs we print its message // Run build flow and ensure that if any error occurs we print its message
start_time := time.now() start_time := time.now()
@ -138,51 +570,8 @@ pub fn build(mut build_config config.BuildConfig) ! {
// Auto-discover sources if not specified // Auto-discover sources if not specified
auto_discover_sources(mut build_config) auto_discover_sources(mut build_config)
graph := plan_build_graph(&build_config)!
// Build shared libraries first (from config) execute_build_graph(mut build_config, graph)!
mut shared_libs_built := []string{}
for mut lib_config in build_config.shared_libs {
if lib_config.sources.len == 0 {
if build_config.debug || build_config.verbose || lib_config.debug || lib_config.verbose {
println('Skipping empty shared library: ${lib_config.name}')
}
continue
}
if build_config.debug || build_config.verbose || lib_config.debug || lib_config.verbose {
println('Building shared library: ${lib_config.name}')
}
build_shared_library(mut lib_config, build_config) or {
return error('Failed to build shared library ${lib_config.name}: ${err}')
}
shared_libs_built << lib_config.name
if build_config.verbose {
println('Built shared library: ${lib_config.name}')
}
}
// Build targets from build directives
build_from_directives(mut build_config, mut shared_libs_built)!
// Build tools/executables from config
for mut tool_config in build_config.tools {
if tool_config.sources.len == 0 {
if build_config.debug || build_config.verbose || tool_config.debug || tool_config.verbose {
println('Skipping empty tool: ${tool_config.name}')
}
continue
}
if build_config.debug || build_config.verbose || tool_config.debug || tool_config.verbose {
println('Building tool: ${tool_config.name}')
}
build_tool(mut tool_config, build_config) or {
return error('Failed to build tool ${tool_config.name}: ${err}')
}
if build_config.verbose {
println('Built tool: ${tool_config.name}')
}
}
return return
} }
@ -201,183 +590,6 @@ pub fn build(mut build_config config.BuildConfig) ! {
println('Build time: ${elapsed.seconds():.2f}s') println('Build time: ${elapsed.seconds():.2f}s')
} }
// Build targets based on build directives from source files
fn build_from_directives(mut build_config config.BuildConfig, mut shared_libs_built []string) ! {
// Build a dependency graph from directives
mut dep_graph := map[string]config.BuildDirective{}
mut build_order := []string{}
mut built_units := []string{}
// Initialize graph with all directives
for directive in build_config.build_directives {
dep_graph[directive.unit_name] = directive
}
// Topological sort to determine build order
for unit_name, _ in dep_graph {
if unit_name in built_units {
continue
}
build_unit_recursive(unit_name, dep_graph, mut build_order, mut built_units, mut build_config, shared_libs_built)!
}
// Build units in determined order
for unit_name in build_order {
directive := dep_graph[unit_name]
if build_config.debug || build_config.verbose {
println('Building unit: ${unit_name}')
}
// Find source file for this unit
mut source_file := ''
// First try the full unit path (e.g., src/lib/file.cpp)
mut candidate := os.join_path(build_config.src_dir, directive.unit_name + '.cpp')
if os.is_file(candidate) {
source_file = candidate
} else {
candidate = os.join_path(build_config.src_dir, directive.unit_name + '.cc')
if os.is_file(candidate) {
source_file = candidate
} else {
candidate = os.join_path(build_config.src_dir, directive.unit_name + '.cxx')
if os.is_file(candidate) {
source_file = candidate
}
}
}
// Fallback: try only the basename (e.g., src/file.cpp) for legacy layouts
if source_file == '' {
parts := directive.unit_name.split('/')
base := if parts.len > 0 { parts[parts.len - 1] } else { directive.unit_name }
candidate = os.join_path(build_config.src_dir, base + '.cpp')
if os.is_file(candidate) {
source_file = candidate
} else {
candidate = os.join_path(build_config.src_dir, base + '.cc')
if os.is_file(candidate) {
source_file = candidate
} else {
candidate = os.join_path(build_config.src_dir, base + '.cxx')
if os.is_file(candidate) {
source_file = candidate
}
}
}
}
if source_file == '' {
if build_config.verbose {
println(colorize('Warning: Source file not found for unit ${unit_name}', ansi_yellow))
}
continue
}
// Create object directory
object_dir := os.join_path(build_config.build_dir, directive.unit_name)
os.mkdir_all(object_dir) or { return error('Failed to create object directory: ${object_dir}') }
obj_file := get_object_file(source_file, object_dir)
// Compile source file
if needs_recompile(source_file, obj_file) {
if build_config.debug || build_config.verbose {
println('Compiling ${unit_name}: ${source_file}...')
}
target_config := config.TargetConfig(config.ToolConfig{
name: unit_name
sources: [source_file]
debug: build_config.debug
optimize: build_config.optimize
verbose: build_config.verbose
cflags: directive.cflags
ldflags: directive.ldflags
})
compile_file(source_file, obj_file, build_config, target_config) or {
return error('Failed to compile ${source_file} for ${unit_name}')
}
} else {
if build_config.verbose {
println('Using cached ${obj_file} for ${unit_name}')
}
}
// Link executable or shared library
if directive.is_shared {
// Link shared library
// place shared libs directly under bin/lib (not nested by unit name)
lib_output_dir := os.join_path(build_config.bin_dir, 'lib')
// ensure output directory exists
os.mkdir_all(lib_output_dir) or { return error('Failed to create shared lib output directory: ${lib_output_dir}') }
if build_config.debug || build_config.verbose {
println('Linking shared library: ${lib_output_dir}/${directive.unit_name.split('/').last()}.so')
}
if build_config.verbose {
// show contents of lib dir for debugging
files := os.ls(lib_output_dir) or { []string{} }
println('Contents of ${lib_output_dir}: ${files}')
}
link_shared_library([obj_file], directive.unit_name, lib_output_dir, build_config, config.SharedLibConfig{
name: directive.unit_name
libraries: directive.link_libs
debug: build_config.debug
optimize: build_config.optimize
verbose: build_config.verbose
ldflags: directive.ldflags
}) or {
return error('Failed to link shared library ${unit_name}')
}
shared_libs_built << directive.unit_name
} else {
// Link executable
executable := os.join_path(build_config.bin_dir, directive.output_path)
if build_config.debug || build_config.verbose {
println('Linking executable: ${executable}')
}
link_tool([obj_file], executable, build_config, config.ToolConfig{
name: directive.unit_name
libraries: directive.link_libs
debug: build_config.debug
optimize: build_config.optimize
verbose: build_config.verbose
ldflags: directive.ldflags
}) or {
return error('Failed to link executable ${unit_name}')
}
}
if build_config.verbose {
println('Successfully built unit: ${unit_name}')
}
}
}
// Recursively build unit and its dependencies
fn build_unit_recursive(unit_name string, dep_graph map[string]config.BuildDirective, mut build_order []string, mut built_units []string, mut build_config config.BuildConfig, shared_libs_built []string) ! {
if unit_name in built_units {
return
}
// Build dependencies first
directive := dep_graph[unit_name]
for dep_unit in directive.depends_units {
if dep_unit in dep_graph {
build_unit_recursive(dep_unit, dep_graph, mut build_order, mut built_units, mut build_config, shared_libs_built)!
} else if !dep_unit.ends_with('.so') && !dep_unit.contains('.') {
// Look for library in shared_libs_built
lib_name := 'lib/${dep_unit}'
if lib_name !in shared_libs_built {
if build_config.verbose {
println(colorize('Warning: Dependency ${dep_unit} not found for ${unit_name}', ansi_yellow))
}
}
}
}
build_order << unit_name
built_units << unit_name
}
fn auto_discover_sources(mut build_config config.BuildConfig) { fn auto_discover_sources(mut build_config config.BuildConfig) {
// Auto-discover shared library sources // Auto-discover shared library sources