From 1a233baed36a9e587edb58faa1f88d91c4974355 Mon Sep 17 00:00:00 2001 From: Jocadbz Date: Tue, 25 Nov 2025 11:49:02 -0300 Subject: [PATCH] Add static linking support and related configuration options --- builder/builder.v | 65 +++++++++++++++- config/config.v | 51 ++++++++++++- docs/help.txt | 1 + docs/templates/config.ini.tpl | 2 + tests/static_link_test.v | 140 ++++++++++++++++++++++++++++++++++ 5 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 tests/static_link_test.v diff --git a/builder/builder.v b/builder/builder.v index e2c67f6..19904f4 100644 --- a/builder/builder.v +++ b/builder/builder.v @@ -592,6 +592,9 @@ fn build_directive_tool(directive config.BuildDirective, build_config config.Bui println('Linking executable: ${executable}') } + // Directive static(true/false) overrides global config, otherwise inherit global setting + use_static := directive.static_link or { build_config.static_link } + link_tool([ctx.object], executable, build_config, toolchain, config.ToolConfig{ name: directive.unit_name libraries: directive.link_libs @@ -599,6 +602,7 @@ fn build_directive_tool(directive config.BuildDirective, build_config config.Bui optimize: build_config.optimize verbose: build_config.verbose ldflags: directive.ldflags + static_link: use_static }) or { return error('Failed to link executable ${directive.unit_name}') } @@ -716,9 +720,25 @@ fn auto_discover_sources(mut build_config config.BuildConfig) { } } + // Also check if the main project exists as a build directive + if !main_tool_exists { + for directive in build_config.build_directives { + // Check if directive unit_name matches project_name or ends with project_name + if directive.unit_name == build_config.project_name { + main_tool_exists = true + break + } + // Also match "fossvg" to directive "fossvg" or "tools/fossvg" + parts := directive.unit_name.split('/') + if parts.len > 0 && parts[parts.len - 1] == 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 { @@ -978,6 +998,49 @@ fn link_shared_library(object_files []string, library_name string, output_path s println(colorize('Linker output:\n${res.output}', ansi_red)) return error('Shared library linking failed with exit code ${res.exit_code}: ${res.output}') } + + // Check if static archive is needed: either global static_link or any tool has static_link + needs_static_archive := build_config.static_link || any_tool_needs_static_link(build_config) + if needs_static_archive { + create_static_archive(object_files, library_name, output_path, build_config, lib_config)! + } +} + +// any_tool_needs_static_link returns true if any tool in the config has static_link enabled +pub fn any_tool_needs_static_link(build_config config.BuildConfig) bool { + for tool in build_config.tools { + if tool.static_link { + return true + } + } + return false +} + +// create_static_archive creates a static library (.a) from object files using ar +fn create_static_archive(object_files []string, library_name string, output_path string, build_config config.BuildConfig, lib_config config.SharedLibConfig) ! { + // Extract base name from library_name (e.g., "lib/cli" -> "cli") + parts := library_name.split('/') + base_name := if parts.len > 0 { parts[parts.len - 1] } else { library_name } + + archive_path := os.join_path(output_path, '${base_name}.a') + + // Build ar command: ar rcs + mut cmd := 'ar rcs ${archive_path}' + for obj_file in object_files { + cmd += ' ${obj_file}' + } + + if lib_config.verbose || build_config.verbose { + println('Creating static archive: ${archive_path}') + println('Archive command: ${cmd}') + } + + ar_res := os.execute(cmd) + if ar_res.exit_code != 0 { + println(colorize('Archive command: ${cmd}', ansi_cyan)) + println(colorize('Archive output:\n${ar_res.output}', ansi_red)) + return error('Static archive creation failed with exit code ${ar_res.exit_code}: ${ar_res.output}') + } } fn link_tool(object_files []string, executable string, build_config config.BuildConfig, toolchain config.Toolchain, tool_config config.ToolConfig) ! { diff --git a/config/config.v b/config/config.v index 1a7a220..ad78505 100644 --- a/config/config.v +++ b/config/config.v @@ -13,6 +13,7 @@ pub mut: cflags []string // file-specific CFLAGS ldflags []string // file-specific LDFLAGS is_shared bool // whether this is a shared library + static_link ?bool // override global static_link setting (none = use global) } // TargetConfig is a sum type for build targets (shared lib or tool) @@ -46,6 +47,7 @@ pub mut: debug bool optimize bool verbose bool + static_link bool // link statically to produce self-contained binary } // Dependency represents an external dependency to download/extract @@ -83,6 +85,7 @@ pub mut: dependencies_dir string = 'dependencies' // external dependencies parallel_compilation bool = true // enable parallel builds dependencies []Dependency + static_link bool // global static linking flag for tools // Build directives from source files build_directives []BuildDirective @@ -100,6 +103,7 @@ mut: optimize_str string verbose_str string parallel_str string + static_link_str string include_dirs []string lib_search_paths []string libraries []string @@ -134,6 +138,7 @@ mut: debug_str string optimize_str string verbose_str string + static_link_str string } struct RawDependencyConfig { @@ -259,6 +264,7 @@ fn normalize_raw_config(raw RawBuildConfig, mut warnings []string) BuildConfig { 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.static_link = resolve_bool(cfg.static_link, raw.global.static_link_str, 'global', 'static_link', mut warnings) 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) @@ -294,6 +300,7 @@ fn normalize_raw_config(raw RawBuildConfig, mut warnings []string) BuildConfig { 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) + static_link: resolve_bool(cfg.static_link, raw_tool.static_link_str, scope, 'static_link', mut warnings) } tool.include_dirs = inherit_list(raw_tool.include_dirs, cfg.include_dirs) tool.cflags = inherit_list(raw_tool.cflags, cfg.cflags) @@ -370,6 +377,8 @@ pub fn (mut build_config BuildConfig) parse_build_directives() ! { mut file_cflags := []string{} mut file_ldflags := []string{} mut is_shared := false + mut static_override := false + mut has_static_override := false for line1 in lines { line := line1.trim_space() @@ -420,6 +429,10 @@ pub fn (mut build_config BuildConfig) parse_build_directives() ! { 'shared' { is_shared = directive_value == 'true' } + 'static' { + static_override = directive_value == 'true' + has_static_override = true + } else { if build_config.verbose { println('Warning: Unknown build directive: ${directive_type} in ${src_file}') @@ -437,6 +450,7 @@ pub fn (mut build_config BuildConfig) parse_build_directives() ! { cflags: file_cflags ldflags: file_ldflags is_shared: is_shared + static_link: if has_static_override { ?bool(static_override) } else { none } } if build_config.verbose { @@ -464,6 +478,7 @@ pub fn parse_args() !BuildConfig { '-O', '--optimize' { build_config.optimize = true; build_config.debug = false } '-v', '--verbose' { build_config.verbose = true } '-p', '--parallel' { build_config.parallel_compilation = true } + '-s', '--static' { build_config.static_link = true } '-o', '--output' { if i + 1 < os.args.len { build_config.project_name = os.args[i + 1] @@ -533,6 +548,7 @@ pub fn parse_args() !BuildConfig { debug: build_config.debug optimize: build_config.optimize verbose: build_config.verbose + static_link: build_config.static_link } build_config.tools << tool_config i += 2 @@ -551,6 +567,7 @@ pub fn parse_args() !BuildConfig { debug: build_config.debug optimize: build_config.optimize verbose: build_config.verbose + static_link: build_config.static_link } build_config.tools << default_tool } @@ -571,6 +588,7 @@ pub fn parse_args() !BuildConfig { debug: build_config.debug optimize: build_config.optimize verbose: build_config.verbose + static_link: build_config.static_link } build_config.tools << default_tool } @@ -657,6 +675,7 @@ pub fn parse_config_file(filename string) !BuildConfig { 'cflags' { raw.global.cflags << parse_space_list(value) } 'ldflags' { raw.global.ldflags << parse_space_list(value) } 'dependencies_dir' { raw.global.dependencies_dir = value } + 'static_link' { raw.global.static_link_str = value } else { warnings << 'Unknown global config key: ${key}' } } } @@ -697,6 +716,7 @@ pub fn parse_config_file(filename string) !BuildConfig { 'optimize' { tool.optimize_str = value } 'verbose' { tool.verbose_str = value } 'output_dir' { tool.output_dir = value } + 'static_link' { tool.static_link_str = value } else { warnings << 'Unknown tools key: ${key}' } } } @@ -935,6 +955,11 @@ fn common_tool_link_command(compiler string, object_files []string, executable s } mut cmd := '${binary}' + // Add static linking flags if enabled + if tool_config.static_link { + cmd += ' -static' + } + cmd += ' -L${build_config.bin_dir}/lib' for lib_path in build_config.lib_search_paths { cmd += ' -L${lib_path}' @@ -954,7 +979,19 @@ fn common_tool_link_command(compiler string, object_files []string, executable s } } - for library in tool_config.libraries { + // For static linking, reverse library order so dependencies come after dependents + // (static linker resolves symbols left-to-right, so A depending on B needs: -lA -lB) + libs := if tool_config.static_link { + mut reversed := []string{} + for i := tool_config.libraries.len - 1; i >= 0; i-- { + reversed << tool_config.libraries[i] + } + reversed + } else { + tool_config.libraries + } + + for library in libs { if library != '' { mut libfile := library if libfile.starts_with('lib/') { @@ -964,7 +1001,12 @@ fn common_tool_link_command(compiler string, object_files []string, executable s if libfile.ends_with('.so') { libfile = libfile.replace('.so', '') } - cmd += ' -l:${libfile}.so' + // Use static library (.a) when static linking, otherwise shared (.so) + if tool_config.static_link { + cmd += ' -l:${libfile}.a' + } else { + cmd += ' -l:${libfile}.so' + } } } @@ -975,6 +1017,11 @@ fn common_tool_link_command(compiler string, object_files []string, executable s cmd += ' ${flag}' } + // Add static runtime flags for fully static binary + if tool_config.static_link { + cmd += ' -static-libgcc -static-libstdc++' + } + cmd += ' -o ${executable}' return cmd } diff --git a/docs/help.txt b/docs/help.txt index 425cd3c..451b298 100644 --- a/docs/help.txt +++ b/docs/help.txt @@ -14,6 +14,7 @@ Global Options: -O, --optimize Enable optimization (-O3, disables debug). -v, --verbose Verbose logging (graph + compiler commands). -p, --parallel Force parallel compilation worker pool. + -s, --static Link tools statically (self-contained binaries). -o, --output Override project/output name. -I Add include directory (repeatable). -L Add library search path (repeatable). diff --git a/docs/templates/config.ini.tpl b/docs/templates/config.ini.tpl index a06a554..cb7fec8 100644 --- a/docs/templates/config.ini.tpl +++ b/docs/templates/config.ini.tpl @@ -10,6 +10,7 @@ debug = true optimize = false verbose = false parallel_compilation = true +static_link = false include_dirs = include lib_search_paths = cflags = -Wall -Wextra @@ -22,3 +23,4 @@ dependencies_dir = dependencies [tools] # legacy/manual entries go here when you don't want build directives +# Per-tool static_link can override global: static_link = true diff --git a/tests/static_link_test.v b/tests/static_link_test.v new file mode 100644 index 0000000..9355a95 --- /dev/null +++ b/tests/static_link_test.v @@ -0,0 +1,140 @@ +module tests + +import config +import builder + +fn test_static_link_defaults_to_false() { + cfg := config.default_config + + assert cfg.static_link == false +} + +fn test_tool_config_static_link_defaults_to_false() { + tool := config.ToolConfig{ + name: 'test' + } + + assert tool.static_link == false +} + +fn test_static_link_command_includes_static_flag() { + cfg := config.BuildConfig{ + compiler: 'g++' + toolchain: 'gcc' + bin_dir: 'bin' + } + tc := config.get_toolchain(cfg) + + tool_cfg := config.ToolConfig{ + name: 'mytool' + static_link: true + } + + cmd := tc.tool_link_command(['main.o'], 'bin/tools/mytool', &cfg, tool_cfg) + + assert cmd.contains('-static') + assert cmd.contains('-static-libgcc') + assert cmd.contains('-static-libstdc++') +} + +fn test_static_link_command_uses_static_libraries() { + cfg := config.BuildConfig{ + compiler: 'g++' + toolchain: 'gcc' + bin_dir: 'bin' + } + tc := config.get_toolchain(cfg) + + tool_cfg := config.ToolConfig{ + name: 'mytool' + libraries: ['core'] + static_link: true + } + + cmd := tc.tool_link_command(['main.o'], 'bin/tools/mytool', &cfg, tool_cfg) + + assert cmd.contains('-l:core.a') + assert !cmd.contains('-l:core.so') +} + +fn test_dynamic_link_command_uses_shared_libraries() { + cfg := config.BuildConfig{ + compiler: 'g++' + toolchain: 'gcc' + bin_dir: 'bin' + } + tc := config.get_toolchain(cfg) + + tool_cfg := config.ToolConfig{ + name: 'mytool' + libraries: ['core'] + static_link: false + } + + cmd := tc.tool_link_command(['main.o'], 'bin/tools/mytool', &cfg, tool_cfg) + + assert cmd.contains('-l:core.so') + assert !cmd.contains('-l:core.a') + assert !cmd.contains('-static') +} + +fn test_clang_static_link_command() { + cfg := config.BuildConfig{ + compiler: 'clang++' + toolchain: 'clang' + bin_dir: 'bin' + } + tc := config.get_toolchain(cfg) + + tool_cfg := config.ToolConfig{ + name: 'mytool' + static_link: true + } + + cmd := tc.tool_link_command(['main.o'], 'bin/tools/mytool', &cfg, tool_cfg) + + assert cmd.starts_with('clang++') + assert cmd.contains('-static') +} + +fn test_any_tool_needs_static_link_returns_false_when_no_tools() { + cfg := config.BuildConfig{ + tools: [] + } + + assert builder.any_tool_needs_static_link(cfg) == false +} + +fn test_any_tool_needs_static_link_returns_false_when_all_dynamic() { + cfg := config.BuildConfig{ + tools: [ + config.ToolConfig{ name: 'tool1', static_link: false }, + config.ToolConfig{ name: 'tool2', static_link: false } + ] + } + + assert builder.any_tool_needs_static_link(cfg) == false +} + +fn test_any_tool_needs_static_link_returns_true_when_one_static() { + cfg := config.BuildConfig{ + tools: [ + config.ToolConfig{ name: 'tool1', static_link: false }, + config.ToolConfig{ name: 'tool2', static_link: true }, + config.ToolConfig{ name: 'tool3', static_link: false } + ] + } + + assert builder.any_tool_needs_static_link(cfg) == true +} + +fn test_any_tool_needs_static_link_returns_true_when_all_static() { + cfg := config.BuildConfig{ + tools: [ + config.ToolConfig{ name: 'tool1', static_link: true }, + config.ToolConfig{ name: 'tool2', static_link: true } + ] + } + + assert builder.any_tool_needs_static_link(cfg) == true +}