diff --git a/builder/builder.v b/builder/builder.v index c9220c1..18ea83e 100644 --- a/builder/builder.v +++ b/builder/builder.v @@ -38,11 +38,39 @@ struct CompileResult { 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_red = '\x1b[31m' const ansi_green = '\x1b[32m' const ansi_yellow = '\x1b[33m' const ansi_cyan = '\x1b[36m' +const skip_directive_err = 'skip_directive' fn should_use_color() bool { 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}' } +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 { mut object_files := []string{} if tasks.len == 0 { @@ -124,6 +235,327 @@ fn run_compile_tasks(tasks []CompileTask, build_config config.BuildConfig) ![]st 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) ! { // Run build flow and ensure that if any error occurs we print its message 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(mut build_config) - - // Build shared libraries first (from config) - 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}') - } - } + graph := plan_build_graph(&build_config)! + execute_build_graph(mut build_config, graph)! return } @@ -201,183 +590,6 @@ pub fn build(mut build_config config.BuildConfig) ! { 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) { // Auto-discover shared library sources