From 4347a882a2da78e84433325bb5fefd756c9ea82d Mon Sep 17 00:00:00 2001 From: Jocadbz Date: Sat, 22 Nov 2025 22:10:32 -0300 Subject: [PATCH] Add build graph preview functionality and enhance configuration parsing - Introduced GraphNodeSummary and GraphPlanSummary structures for build graph representation. - Implemented preview_build_graph function to generate a summary of the build graph. - Refactored configuration parsing to improve handling of boolean values and inheritance of lists. - Added unit tests for build graph preview and configuration parsing. I still don't know how to write tests. --- builder/builder.v | 82 ++++++++++++++++++++++++ config/config.v | 134 ++++++++++++++------------------------- deps/deps.v | 26 ++++---- tests/build_graph_test.v | 70 ++++++++++++++++++++ tests/config_test.v | 89 ++++++++++++++++++++++++++ tests/deps_test.v | 45 +++++++++++++ tests/discovery_test.v | 47 ++++++++++++++ tests/test_helpers.v | 11 ++++ 8 files changed, 405 insertions(+), 99 deletions(-) create mode 100644 tests/build_graph_test.v create mode 100644 tests/config_test.v create mode 100644 tests/deps_test.v create mode 100644 tests/discovery_test.v create mode 100644 tests/test_helpers.v diff --git a/builder/builder.v b/builder/builder.v index 94a6c16..3a6482a 100644 --- a/builder/builder.v +++ b/builder/builder.v @@ -59,6 +59,57 @@ struct BuildGraph { unresolved map[string][]string } +pub struct GraphNodeSummary { +pub: + id string + name string + target BuildTarget + dependencies []string + raw_dependencies []string + is_directive bool + output_path string +} + +pub struct GraphPlanSummary { +pub: + nodes []GraphNodeSummary + order []string + unresolved map[string][]string +} + +pub fn preview_build_graph(build_config &config.BuildConfig) !GraphPlanSummary { + graph := plan_build_graph(build_config)! + + mut nodes := []GraphNodeSummary{} + for node in graph.nodes { + nodes << GraphNodeSummary{ + id: node.id + name: node.name + target: node.target + dependencies: node.dependencies.clone() + raw_dependencies: node.raw_dependencies.clone() + is_directive: node.is_directive + output_path: node.output_path + } + } + + mut order := []string{} + for idx in graph.order { + order << graph.nodes[idx].id + } + + mut unresolved := map[string][]string{} + for id, deps in graph.unresolved { + unresolved[id] = deps.clone() + } + + return GraphPlanSummary{ + nodes: nodes + order: order + unresolved: unresolved + } +} + struct DirectiveBuildContext { source string object string @@ -653,6 +704,37 @@ fn auto_discover_sources(mut build_config config.BuildConfig) { } } } + + // Ensure the main project tool exists if it wasn't explicitly defined + // This handles the case where config.ini defines other tools but not the main project tool + mut main_tool_exists := false + for tool in build_config.tools { + if tool.name == build_config.project_name { + main_tool_exists = true + break + } + } + + if !main_tool_exists { + // Look for src/.cpp + // Really shit way to do it but meh, we'll figure this out soon enough. + main_src := os.join_path(build_config.src_dir, '${build_config.project_name}.cpp') + if os.is_file(main_src) { + if build_config.verbose { + println('Auto-discovered main project tool: ${build_config.project_name} from ${main_src}') + } + + new_tool := config.ToolConfig{ + name: build_config.project_name + sources: [main_src] + debug: build_config.debug + optimize: build_config.optimize + verbose: build_config.verbose + libraries: [] // Will inherit global libs during link + } + build_config.tools << new_tool + } + } } pub fn clean(build_config config.BuildConfig) { diff --git a/config/config.v b/config/config.v index 3546594..b28496f 100644 --- a/config/config.v +++ b/config/config.v @@ -203,6 +203,30 @@ fn merge_unique(mut target []string, additions []string) { } } +fn resolve_bool(base bool, override_str string, scope string, field string, mut warnings []string) bool { + if override_str.trim_space() == '' { + return base + } + return parse_bool_str(override_str) or { + warnings << 'Invalid boolean value for ${scope} ${field}: ${override_str}' + base + } +} + +fn inherit_list(primary []string, inherited []string) []string { + mut merged := primary.clone() + merge_unique(mut merged, inherited) + return merged +} + +fn scope_label(name string, kind string) string { + trimmed := name.trim_space() + if trimmed != '' { + return '${kind} ${trimmed}' + } + return kind +} + fn normalize_raw_config(raw RawBuildConfig, mut warnings []string) BuildConfig { mut cfg := default_config default_shared := SharedLibConfig{} @@ -230,113 +254,49 @@ fn normalize_raw_config(raw RawBuildConfig, mut warnings []string) BuildConfig { cfg.dependencies_dir = raw.global.dependencies_dir } - if raw.global.debug_str != '' { - cfg.debug = parse_bool_str(raw.global.debug_str) or { - warnings << 'Invalid boolean value for global.debug: ${raw.global.debug_str}' - cfg.debug - } - } - if raw.global.optimize_str != '' { - cfg.optimize = parse_bool_str(raw.global.optimize_str) or { - warnings << 'Invalid boolean value for global.optimize: ${raw.global.optimize_str}' - cfg.optimize - } - } - if raw.global.verbose_str != '' { - cfg.verbose = parse_bool_str(raw.global.verbose_str) or { - warnings << 'Invalid boolean value for global.verbose: ${raw.global.verbose_str}' - cfg.verbose - } - } - if raw.global.parallel_str != '' { - cfg.parallel_compilation = parse_bool_str(raw.global.parallel_str) or { - warnings << 'Invalid boolean value for global.parallel_compilation: ${raw.global.parallel_str}' - cfg.parallel_compilation - } - } + cfg.debug = resolve_bool(cfg.debug, raw.global.debug_str, 'global', 'debug', mut warnings) + cfg.optimize = resolve_bool(cfg.optimize, raw.global.optimize_str, 'global', 'optimize', mut warnings) + cfg.verbose = resolve_bool(cfg.verbose, raw.global.verbose_str, 'global', 'verbose', mut warnings) + cfg.parallel_compilation = resolve_bool(cfg.parallel_compilation, raw.global.parallel_str, 'global', 'parallel_compilation', mut warnings) - cfg.include_dirs << raw.global.include_dirs - cfg.lib_search_paths << raw.global.lib_search_paths - cfg.libraries << raw.global.libraries - cfg.cflags << raw.global.cflags - cfg.ldflags << raw.global.ldflags + cfg.include_dirs = inherit_list(raw.global.include_dirs, cfg.include_dirs) + cfg.lib_search_paths = inherit_list(raw.global.lib_search_paths, cfg.lib_search_paths) + cfg.libraries = inherit_list(raw.global.libraries, cfg.libraries) + cfg.cflags = inherit_list(raw.global.cflags, cfg.cflags) + cfg.ldflags = inherit_list(raw.global.ldflags, cfg.ldflags) for raw_lib in raw.shared_libs { + scope := scope_label(raw_lib.name, 'shared_lib') mut lib := SharedLibConfig{ name: raw_lib.name output_dir: if raw_lib.output_dir != '' { raw_lib.output_dir } else { default_shared.output_dir } sources: raw_lib.sources.clone() libraries: raw_lib.libraries.clone() - debug: cfg.debug - optimize: cfg.optimize - verbose: cfg.verbose + debug: resolve_bool(cfg.debug, raw_lib.debug_str, scope, 'debug', mut warnings) + optimize: resolve_bool(cfg.optimize, raw_lib.optimize_str, scope, 'optimize', mut warnings) + verbose: resolve_bool(cfg.verbose, raw_lib.verbose_str, scope, 'verbose', mut warnings) } - lib.include_dirs = raw_lib.include_dirs.clone() - lib.cflags = raw_lib.cflags.clone() - lib.ldflags = raw_lib.ldflags.clone() - - if raw_lib.debug_str != '' { - lib.debug = parse_bool_str(raw_lib.debug_str) or { - warnings << 'Invalid boolean value for shared_lib ${raw_lib.name} debug: ${raw_lib.debug_str}' - lib.debug - } - } - if raw_lib.optimize_str != '' { - lib.optimize = parse_bool_str(raw_lib.optimize_str) or { - warnings << 'Invalid boolean value for shared_lib ${raw_lib.name} optimize: ${raw_lib.optimize_str}' - lib.optimize - } - } - if raw_lib.verbose_str != '' { - lib.verbose = parse_bool_str(raw_lib.verbose_str) or { - warnings << 'Invalid boolean value for shared_lib ${raw_lib.name} verbose: ${raw_lib.verbose_str}' - lib.verbose - } - } - - merge_unique(mut lib.include_dirs, cfg.include_dirs) - merge_unique(mut lib.cflags, cfg.cflags) - merge_unique(mut lib.ldflags, cfg.ldflags) + lib.include_dirs = inherit_list(raw_lib.include_dirs, cfg.include_dirs) + lib.cflags = inherit_list(raw_lib.cflags, cfg.cflags) + lib.ldflags = inherit_list(raw_lib.ldflags, cfg.ldflags) cfg.shared_libs << lib } for raw_tool in raw.tools { + scope := scope_label(raw_tool.name, 'tool') mut tool := ToolConfig{ name: raw_tool.name output_dir: if raw_tool.output_dir != '' { raw_tool.output_dir } else { default_tool.output_dir } sources: raw_tool.sources.clone() libraries: raw_tool.libraries.clone() - debug: cfg.debug - optimize: cfg.optimize - verbose: cfg.verbose + debug: resolve_bool(cfg.debug, raw_tool.debug_str, scope, 'debug', mut warnings) + optimize: resolve_bool(cfg.optimize, raw_tool.optimize_str, scope, 'optimize', mut warnings) + verbose: resolve_bool(cfg.verbose, raw_tool.verbose_str, scope, 'verbose', mut warnings) } - tool.include_dirs = raw_tool.include_dirs.clone() - tool.cflags = raw_tool.cflags.clone() - tool.ldflags = raw_tool.ldflags.clone() - - if raw_tool.debug_str != '' { - tool.debug = parse_bool_str(raw_tool.debug_str) or { - warnings << 'Invalid boolean value for tool ${raw_tool.name} debug: ${raw_tool.debug_str}' - tool.debug - } - } - if raw_tool.optimize_str != '' { - tool.optimize = parse_bool_str(raw_tool.optimize_str) or { - warnings << 'Invalid boolean value for tool ${raw_tool.name} optimize: ${raw_tool.optimize_str}' - tool.optimize - } - } - if raw_tool.verbose_str != '' { - tool.verbose = parse_bool_str(raw_tool.verbose_str) or { - warnings << 'Invalid boolean value for tool ${raw_tool.name} verbose: ${raw_tool.verbose_str}' - tool.verbose - } - } - - merge_unique(mut tool.include_dirs, cfg.include_dirs) - merge_unique(mut tool.cflags, cfg.cflags) - merge_unique(mut tool.ldflags, cfg.ldflags) + tool.include_dirs = inherit_list(raw_tool.include_dirs, cfg.include_dirs) + tool.cflags = inherit_list(raw_tool.cflags, cfg.cflags) + tool.ldflags = inherit_list(raw_tool.ldflags, cfg.ldflags) cfg.tools << tool } diff --git a/deps/deps.v b/deps/deps.v index 60f3bbb..7a39065 100644 --- a/deps/deps.v +++ b/deps/deps.v @@ -23,31 +23,33 @@ pub fn extract_dependencies(source_file string) ![]string { in_string = false current_string_char = rune(0) } else if !in_string { - if c == `#` && i + 1 < content.len && content[i + 1] == `i` { - // Found #include - i += 7 // skip "#include" + if c == `#` && i + 8 <= content.len && content[i..].starts_with('#include') { + i += '#include'.len for i < content.len && content[i].is_space() { i++ } - - if i < content.len && (content[i] == `"` || content[i] == `<`) { - mut quote_char := content[i] + + if i < content.len && (content[i] == `"` || content[i] == `<` || content[i] == `'`) { + opening := content[i] + closing := match opening { + `"` { `"` } + `'` { `'` } + `<` { `>` } + else { opening } + } i++ mut include_path := []u8{} - - for i < content.len && content[i] != quote_char { + + for i < content.len && content[i] != closing { include_path << content[i] i++ } - + if include_path.len > 0 { include_name := include_path.bytestr() if include_name.contains('/') || include_name.contains('\\') { - // Relative path dependencies << include_name } else { - // System include - we could search standard paths - // but for now just add the name dependencies << include_name } } diff --git a/tests/build_graph_test.v b/tests/build_graph_test.v new file mode 100644 index 0000000..e5a0826 --- /dev/null +++ b/tests/build_graph_test.v @@ -0,0 +1,70 @@ +module tests + +import os +import builder +import config + +fn test_preview_build_graph_orders_dependencies() { + tmp := new_temp_dir('lana_graph') + defer { + os.rmdir_all(tmp) or {} + } + + src_dir := os.join_path(tmp, 'src') + lib_dir := os.join_path(src_dir, 'lib') + tool_dir := os.join_path(src_dir, 'tools') + os.mkdir_all(lib_dir) or { panic(err) } + os.mkdir_all(tool_dir) or { panic(err) } + + core_src := os.join_path(lib_dir, 'core.cpp') + tool_src := os.join_path(tool_dir, 'demo.cpp') + os.write_file(core_src, '// core stub\n') or { panic(err) } + os.write_file(tool_src, '// tool stub\n') or { panic(err) } + + mut cfg := config.BuildConfig{ + project_name: 'demo' + src_dir: src_dir + build_dir: os.join_path(tmp, 'build') + bin_dir: os.join_path(tmp, 'bin') + toolchain: 'gcc' + compiler: 'g++' + debug: true + shared_libs: [ + config.SharedLibConfig{ + name: 'core' + sources: [core_src] + } + ] + tools: [ + config.ToolConfig{ + name: 'demo' + sources: [tool_src] + libraries: ['core'] + } + ] + } + + summary := builder.preview_build_graph(&cfg) or { panic(err) } + + assert summary.nodes.len == 2 + assert summary.unresolved.len == 0 + + mut shared_found := false + mut tool_found := false + for node in summary.nodes { + if node.id == 'shared:core' { + shared_found = true + assert node.dependencies.len == 0 + } + if node.id == 'tool:demo' { + tool_found = true + assert node.dependencies.contains('shared:core') + } + } + + assert shared_found + assert tool_found + assert summary.order.len == 2 + assert summary.order[0] == 'shared:core' + assert summary.order[1] == 'tool:demo' +} diff --git a/tests/config_test.v b/tests/config_test.v new file mode 100644 index 0000000..2289250 --- /dev/null +++ b/tests/config_test.v @@ -0,0 +1,89 @@ +module tests + +import os +import config + +fn test_parse_config_file_reads_sections() { + tmp := new_temp_dir('lana_config') + defer { + os.rmdir_all(tmp) or {} + } + + config_path := os.join_path(tmp, 'config.ini') + config_content := '[global]\nproject_name = sample\nsrc_dir = src\ndebug = false\noptimize = true\nparallel_compilation = false\ninclude_dirs = include, external/include\nlibraries = cli\ncflags = -Wall -Wextra\n\n[shared_libs]\nname = core\nsources = src/lib/core.cpp\ninclude_dirs = include\ncflags = -fPIC\nlibraries = \n\n[tools]\nname = sample\nsources = src/main.cpp\nlibraries = core\n' + os.write_file(config_path, config_content) or { panic(err) } + + cfg := config.parse_config_file(config_path) or { panic(err) } + + assert cfg.project_name == 'sample' + assert cfg.src_dir == 'src' + assert !cfg.debug + assert cfg.optimize + assert !cfg.parallel_compilation + assert cfg.include_dirs.len == 2 + assert cfg.include_dirs.contains('include') + assert cfg.include_dirs.contains('external/include') + assert cfg.cflags.contains('-Wall') + assert cfg.cflags.contains('-Wextra') + assert cfg.libraries == ['cli'] + assert cfg.shared_libs.len == 1 + shared_cfg := cfg.shared_libs[0] + assert shared_cfg.name == 'core' + assert shared_cfg.include_dirs.contains('include') + assert shared_cfg.cflags.contains('-fPIC') + assert cfg.tools.len == 1 + tool := cfg.tools[0] + assert tool.name == 'sample' + assert tool.libraries == ['core'] +} + +fn test_parse_build_directives_extracts_units() { + tmp := new_temp_dir('lana_directives') + defer { + os.rmdir_all(tmp) or {} + } + + src_dir := os.join_path(tmp, 'src') + tool_dir := os.join_path(src_dir, 'tools') + lib_dir := os.join_path(src_dir, 'lib') + os.mkdir_all(tool_dir) or { panic(err) } + os.mkdir_all(lib_dir) or { panic(err) } + + tool_source := '// build-directive: unit-name(tools/example)\n// build-directive: depends-units(lib/cli)\n// build-directive: link(cli.so)\n// build-directive: out(tools/example)\n// build-directive: cflags(-DTEST)\n// build-directive: ldflags(-pthread)\n// build-directive: shared(false)\n' + os.write_file(os.join_path(tool_dir, 'example.cpp'), tool_source) or { panic(err) } + + shared_source := '// build-directive: unit-name(lib/cli)\n// build-directive: depends-units()\n// build-directive: link()\n// build-directive: out(lib/cli)\n// build-directive: shared(true)\n' + os.write_file(os.join_path(lib_dir, 'cli.cpp'), shared_source) or { panic(err) } + + mut cfg := config.BuildConfig{ + project_name: 'sample' + src_dir: src_dir + build_dir: os.join_path(tmp, 'build') + bin_dir: os.join_path(tmp, 'bin') + } + + cfg.parse_build_directives() or { panic(err) } + + assert cfg.build_directives.len == 2 + + mut tool_found := false + mut shared_found := false + for directive in cfg.build_directives { + if directive.unit_name == 'tools/example' { + tool_found = true + assert directive.depends_units == ['lib/cli'] + assert directive.link_libs == ['cli.so'] + assert directive.output_path == 'tools/example' + assert directive.cflags.contains('-DTEST') + assert directive.ldflags.contains('-pthread') + assert !directive.is_shared + } + if directive.unit_name == 'lib/cli' { + shared_found = true + assert directive.is_shared + } + } + + assert tool_found + assert shared_found +} diff --git a/tests/deps_test.v b/tests/deps_test.v new file mode 100644 index 0000000..5b5b2f0 --- /dev/null +++ b/tests/deps_test.v @@ -0,0 +1,45 @@ +module tests + +import os +import deps +import config + +fn test_extract_dependencies_finds_includes() { + tmp := new_temp_dir('lana_deps') + defer { + os.rmdir_all(tmp) or {} + } + + source_path := os.join_path(tmp, 'sample.cpp') + source_content := '#include "foo/bar.h"\n#include \nint main() { return 0; }\n' + os.write_file(source_path, source_content) or { panic(err) } + + parsed := deps.extract_dependencies(source_path) or { panic(err) } + + assert parsed.any(it.contains('foo/bar.h')) + assert parsed.any(it.contains('vector')) +} + +fn test_fetch_dependencies_runs_build_commands() { + tmp := new_temp_dir('lana_fetch') + defer { + os.rmdir_all(tmp) or {} + } + + dep_root := os.join_path(tmp, 'deps') + mut cfg := config.BuildConfig{ + dependencies_dir: dep_root + dependencies: [ + config.Dependency{ + name: 'local' + extract_to: 'localpkg' + build_cmds: ['touch built.txt'] + } + ] + } + + deps.fetch_dependencies(cfg) or { panic(err) } + + assert os.is_dir(os.join_path(dep_root, 'localpkg')) + assert os.is_file(os.join_path(dep_root, 'localpkg', 'built.txt')) +} diff --git a/tests/discovery_test.v b/tests/discovery_test.v new file mode 100644 index 0000000..b13aa8c --- /dev/null +++ b/tests/discovery_test.v @@ -0,0 +1,47 @@ +module tests + +import os +import builder +import config + +fn test_auto_discover_main_tool_when_other_tools_exist() { + tmp := new_temp_dir('lana_discovery') + defer { + os.rmdir_all(tmp) or {} + } + + src_dir := os.join_path(tmp, 'src') + tools_dir := os.join_path(src_dir, 'tools') + os.mkdir_all(tools_dir) or { panic(err) } + + // Create a tool that IS in config + os.write_file(os.join_path(tools_dir, 'existing.cpp'), 'int main(){}') or { panic(err) } + + // Create a main source that SHOULD be discovered as the main tool + os.write_file(os.join_path(src_dir, 'testproj.cpp'), 'int main(){}') or { panic(err) } + + mut cfg := config.BuildConfig{ + project_name: 'testproj' + src_dir: src_dir + tools: [ + config.ToolConfig{ + name: 'existing' + sources: [os.join_path(tools_dir, 'existing.cpp')] + } + ] + } + + // Run auto-discovery + builder.auto_discover_sources(mut cfg) + + // Check if testproj tool was added + mut found := false + for tool in cfg.tools { + if tool.name == 'testproj' { + found = true + assert tool.sources.len == 1 + assert tool.sources[0].ends_with('testproj.cpp') + } + } + assert found +} diff --git a/tests/test_helpers.v b/tests/test_helpers.v new file mode 100644 index 0000000..b44d520 --- /dev/null +++ b/tests/test_helpers.v @@ -0,0 +1,11 @@ +module tests + +import os +import rand + +fn new_temp_dir(prefix string) string { + id := rand.uuid_v4() + path := os.join_path(os.temp_dir(), '${prefix}_${id}') + os.mkdir_all(path) or { panic(err) } + return path +}