Add static linking support and related configuration options

main
Joca 2025-11-25 11:49:02 -03:00
parent f74b93ddf6
commit 1a233baed3
Signed by: jocadbz
GPG Key ID: B1836DCE2F50BDF7
5 changed files with 256 additions and 3 deletions

View File

@ -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/<project_name>.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 <archive> <objects...>
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) ! {

View File

@ -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,9 +1001,14 @@ fn common_tool_link_command(compiler string, object_files []string, executable s
if libfile.ends_with('.so') {
libfile = libfile.replace('.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'
}
}
}
for flag in build_config.ldflags {
cmd += ' ${flag}'
@ -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
}

View File

@ -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 <name> Override project/output name.
-I <dir> Add include directory (repeatable).
-L <dir> Add library search path (repeatable).

View File

@ -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

140
tests/static_link_test.v Normal file
View File

@ -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
}