lana/builder/builder.v

705 lines
24 KiB
V

module builder
import os
import config
import deps
// BuildTarget represents a build target (shared lib or tool)
pub enum BuildTarget {
shared_lib
tool
}
// BuildTargetInfo holds common information for all build targets
pub struct BuildTargetInfo {
name string
sources []string
object_dir string
output_dir string
debug bool
optimize bool
verbose bool
include_dirs []string
cflags []string
ldflags []string
libraries []string
}
pub fn build(mut build_config config.BuildConfig) ! {
println('Building ${build_config.project_name}...')
// Create directories
os.mkdir_all(build_config.build_dir) or { return error('Failed to create build directory') }
os.mkdir_all(build_config.bin_dir) or { return error('Failed to create bin directory') }
os.mkdir_all('${build_config.bin_dir}/lib') or { return error('Failed to create lib directory') }
os.mkdir_all('${build_config.bin_dir}/tools') or { return error('Failed to create tools directory') }
if build_config.shaders_dir != '' && build_config.shaders_dir != 'bin/shaders' {
os.mkdir_all(build_config.shaders_dir) or { return error('Failed to create shaders directory') }
}
// 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.verbose {
println('Skipping empty shared library: ${lib_config.name}')
}
continue
}
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.verbose {
println('Skipping empty tool: ${tool_config.name}')
}
continue
}
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}')
}
}
// Build shaders if configured and directory exists
if build_config.shaders_dir != '' {
compile_shaders(build_config) or {
if !build_config.verbose {
println('Warning: Failed to compile shaders: ${err}')
}
}
}
println('Build completed successfully!')
}
// 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, directive 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]
println('Building unit: ${unit_name}')
// Find source file for this unit
mut source_file := ''
for src_path in directive.unit_name.split('/') {
source_file = os.join_path(build_config.src_dir, src_path + '.cpp')
if os.is_file(source_file) {
break
}
source_file = os.join_path(build_config.src_dir, src_path + '.cc')
if os.is_file(source_file) {
break
}
source_file = os.join_path(build_config.src_dir, src_path + '.cxx')
if os.is_file(source_file) {
break
}
}
if source_file == '' {
if build_config.verbose {
println('Warning: Source file not found for unit ${unit_name}')
}
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) {
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
lib_output := os.join_path(build_config.bin_dir, 'lib', directive.unit_name)
println('Linking shared library: ${lib_output}')
link_shared_library([obj_file], directive.unit_name, lib_output, 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)
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('Warning: Dependency ${dep_unit} not found for ${unit_name}')
}
}
}
}
build_order << unit_name
built_units << unit_name
}
fn auto_discover_sources(mut build_config config.BuildConfig) {
// Auto-discover shared library sources
for mut lib_config in build_config.shared_libs {
if lib_config.sources.len == 0 {
// Look for sources in src/lib/<lib_name>/
lib_src_dir := os.join_path('src', 'lib', lib_config.name)
if os.is_dir(lib_src_dir) {
lib_sources := find_source_files(lib_src_dir) or { []string{} }
lib_config.sources = lib_sources
if build_config.verbose && lib_sources.len > 0 {
println('Auto-discovered ${lib_sources.len} source files for shared lib ${lib_config.name}')
}
}
}
}
// Auto-discover tool sources
for mut tool_config in build_config.tools {
if tool_config.sources.len == 0 {
// Look for sources in src/tools/<tool_name>/
tool_src_dir := os.join_path('src', 'tools', tool_config.name)
if os.is_dir(tool_src_dir) {
tool_sources := find_source_files(tool_src_dir) or { []string{} }
if tool_sources.len > 0 {
tool_config.sources = tool_sources
} else {
// Fallback: look for main.cpp or tool_name.cpp in src/
fallback_sources := [
os.join_path('src', '${tool_config.name}.cpp'),
os.join_path('src', 'main.cpp')
]
for fallback in fallback_sources {
if os.is_file(fallback) {
tool_config.sources << fallback
break
}
}
}
if build_config.verbose && tool_config.sources.len > 0 {
println('Auto-discovered ${tool_config.sources.len} source files for tool ${tool_config.name}')
}
}
}
}
// If still no sources for default tool, use all files in src/
if build_config.tools.len > 0 && build_config.tools[0].sources.len == 0 {
mut default_tool := &build_config.tools[0]
if default_tool.name == build_config.project_name {
all_sources := find_source_files(build_config.src_dir) or { []string{} }
if all_sources.len > 0 {
default_tool.sources = all_sources
if build_config.verbose {
println('Auto-discovered ${all_sources.len} source files for main project')
}
}
}
}
}
pub fn clean(build_config config.BuildConfig) {
println('Cleaning build files...')
// Remove build directory
if os.is_dir(build_config.build_dir) {
os.rmdir_all(build_config.build_dir) or {
println('Warning: Failed to remove ${build_config.build_dir}: ${err}')
}
println('Removed ${build_config.build_dir}')
}
// Remove bin directories
dirs_to_clean := ['lib', 'tools']
for dir in dirs_to_clean {
full_dir := os.join_path(build_config.bin_dir, dir)
if os.is_dir(full_dir) {
os.rmdir_all(full_dir) or {
println('Warning: Failed to remove ${full_dir}: ${err}')
}
println('Removed ${full_dir}')
}
}
// Remove shaders directory if it exists
shaders_dir := if build_config.shaders_dir.starts_with('bin/') {
os.join_path(build_config.bin_dir, build_config.shaders_dir[4..])
} else {
build_config.shaders_dir
}
if os.is_dir(shaders_dir) {
os.rmdir_all(shaders_dir) or {
println('Warning: Failed to remove ${shaders_dir}: ${err}')
}
println('Removed ${shaders_dir}')
}
// Remove main executable if it exists (backward compatibility)
main_exe := os.join_path(build_config.bin_dir, build_config.project_name)
if os.is_file(main_exe) {
os.rm(main_exe) or {
println('Warning: Failed to remove ${main_exe}: ${err}')
}
println('Removed ${main_exe}')
}
println('Clean completed!')
}
fn build_shared_library(mut lib_config config.SharedLibConfig, build_config config.BuildConfig) ! {
if lib_config.sources.len == 0 {
if build_config.verbose {
println('No sources specified for shared library ${lib_config.name}, skipping')
}
return
}
// Create output directory
os.mkdir_all(lib_config.output_dir) or { return error('Failed to create shared lib directory: ${lib_config.output_dir}') }
mut object_files := []string{}
mut object_dir := os.join_path(build_config.build_dir, lib_config.name)
os.mkdir_all(object_dir) or { return error('Failed to create object directory: ${object_dir}') }
// Compile each source file
for src_file in lib_config.sources {
if !os.is_file(src_file) {
if build_config.verbose {
println('Warning: Source file not found: ${src_file}')
}
continue
}
obj_file := get_object_file(src_file, object_dir)
// Create object directory if needed
obj_path := os.dir(obj_file)
os.mkdir_all(obj_path) or { return error('Failed to create object directory: ${obj_path}') }
// Check if we need to recompile
if needs_recompile(src_file, obj_file) {
println('Compiling ${lib_config.name}: ${src_file}...')
lib_target_config := config.TargetConfig(lib_config)
object_files << compile_file(src_file, obj_file, build_config, lib_target_config) or {
return error('Failed to compile ${src_file} for ${lib_config.name}')
}
} else {
if lib_config.verbose {
println('Using cached ${obj_file} for ${lib_config.name}')
}
object_files << obj_file
}
}
if object_files.len == 0 {
return error('No object files generated for shared library ${lib_config.name}')
}
// Link shared library
lib_output := os.join_path(lib_config.output_dir, lib_config.name)
println('Linking shared library: ${lib_output}')
link_shared_library(object_files, lib_config.name, lib_output, build_config, lib_config) or {
return error('Failed to link shared library ${lib_config.name}')
}
if build_config.verbose {
println('Successfully built shared library: ${lib_config.name}')
}
}
fn build_tool(mut tool_config config.ToolConfig, build_config config.BuildConfig) ! {
if tool_config.sources.len == 0 {
if build_config.verbose {
println('No sources specified for tool ${tool_config.name}, skipping')
}
return
}
// Create output directory
os.mkdir_all(tool_config.output_dir) or { return error('Failed to create tool directory: ${tool_config.output_dir}') }
mut object_files := []string{}
mut object_dir := os.join_path(build_config.build_dir, tool_config.name)
os.mkdir_all(object_dir) or { return error('Failed to create object directory: ${object_dir}') }
// Compile each source file
for src_file in tool_config.sources {
if !os.is_file(src_file) {
if build_config.verbose {
println('Warning: Source file not found: ${src_file}')
}
continue
}
obj_file := get_object_file(src_file, object_dir)
// Create object directory if needed
obj_path := os.dir(obj_file)
os.mkdir_all(obj_path) or { return error('Failed to create object directory: ${obj_path}') }
// Check if we need to recompile
if needs_recompile(src_file, obj_file) {
println('Compiling ${tool_config.name}: ${src_file}...')
tool_target_config := config.TargetConfig(tool_config)
object_files << compile_file(src_file, obj_file, build_config, tool_target_config) or {
return error('Failed to compile ${src_file} for ${tool_config.name}')
}
} else {
if tool_config.verbose {
println('Using cached ${obj_file} for ${tool_config.name}')
}
object_files << obj_file
}
}
if object_files.len == 0 {
return error('No object files generated for tool ${tool_config.name}')
}
// Link executable
executable := os.join_path(tool_config.output_dir, tool_config.name)
println('Linking tool: ${executable}')
link_tool(object_files, executable, build_config, tool_config) or {
return error('Failed to link tool ${tool_config.name}')
}
if build_config.verbose {
println('Successfully built tool: ${tool_config.name}')
}
}
// Helper function to get target verbose setting
fn get_target_verbose(target_config config.TargetConfig) bool {
mut verbose := false
match target_config {
config.SharedLibConfig {
verbose = target_config.verbose
}
config.ToolConfig {
verbose = target_config.verbose
}
}
return verbose
}
fn compile_file(source_file string, object_file string, build_config config.BuildConfig, target_config config.TargetConfig) !string {
cmd := config.build_shared_compiler_command(source_file, object_file, build_config, target_config)
target_verbose := get_target_verbose(target_config)
if target_verbose {
println('Compile command: ${cmd}')
}
res := os.execute(cmd)
if res.exit_code != 0 {
return error('Compilation failed with exit code ${res.exit_code}:\n${res.output}')
}
// Generate dependency file
dep_file := object_file.replace('.o', '.d')
deps.generate_dependency_file(source_file, object_file, dep_file)
return object_file
}
fn link_shared_library(object_files []string, library_name string, output_path string, build_config config.BuildConfig, lib_config config.SharedLibConfig) ! {
cmd := config.build_shared_linker_command(object_files, library_name, output_path, build_config, lib_config)
if lib_config.verbose {
println('Shared lib link command: ${cmd}')
}
res := os.execute(cmd)
if res.exit_code != 0 {
return error('Shared library linking failed with exit code ${res.exit_code}:\n${res.output}')
}
}
fn link_tool(object_files []string, executable string, build_config config.BuildConfig, tool_config config.ToolConfig) ! {
cmd := config.build_tool_linker_command(object_files, executable, build_config, tool_config)
if tool_config.verbose {
println('Tool link command: ${cmd}')
}
res := os.execute(cmd)
if res.exit_code != 0 {
return error('Tool linking failed with exit code ${res.exit_code}:\n${res.output}')
}
}
fn compile_shaders(build_config config.BuildConfig) ! {
shaders_src_dir := 'src/shaders'
if !os.is_dir(shaders_src_dir) {
if build_config.verbose {
println('No shaders directory found, skipping shader compilation')
}
return
}
println('Compiling shaders...')
// Find glslc compiler
glslc_path := find_glslc() or {
println('Warning: glslc compiler not found, skipping shader compilation')
return
}
// Get shaders directory
shaders_out_dir := if build_config.shaders_dir.starts_with('bin/') {
os.join_path(build_config.bin_dir, build_config.shaders_dir[4..])
} else {
build_config.shaders_dir
}
os.mkdir_all(shaders_out_dir) or { return error('Failed to create shaders output directory') }
// List all shader files
shader_files := os.ls(shaders_src_dir) or { return error('Failed to list shaders directory') }
mut shader_count := 0
mut success_count := 0
// Compile vertex shaders (.vsh)
for shader in shader_files {
if !shader.ends_with('.vsh') {
continue
}
shader_count++
src_path := os.join_path(shaders_src_dir, shader)
output_name := shader.replace('.vsh', '.vsh.spv')
output_path := os.join_path(shaders_out_dir, output_name)
cmd := '${glslc_path} -o ${output_path} -fshader-stage=vertex ${src_path}'
println('Compiling vertex shader: ${shader}')
if build_config.verbose {
println('Shader compile command: ${cmd}')
}
res := os.execute(cmd)
if res.exit_code != 0 {
println('Error compiling ${shader}: ${res.output}')
} else {
success_count++
if build_config.verbose {
println('Compiled: ${shader} -> ${output_path}')
}
}
}
// Compile fragment shaders (.fsh)
for shader in shader_files {
if !shader.ends_with('.fsh') {
continue
}
shader_count++
src_path := os.join_path(shaders_src_dir, shader)
output_name := shader.replace('.fsh', '.fsh.spv')
output_path := os.join_path(shaders_out_dir, output_name)
cmd := '${glslc_path} -o ${output_path} -fshader-stage=fragment ${src_path}'
println('Compiling fragment shader: ${shader}')
if build_config.verbose {
println('Shader compile command: ${cmd}')
}
res := os.execute(cmd)
if res.exit_code != 0 {
println('Error compiling ${shader}: ${res.output}')
} else {
success_count++
if build_config.verbose {
println('Compiled: ${shader} -> ${output_path}')
}
}
}
if shader_count == 0 {
println('No shaders found to compile')
} else {
println('Shader compilation complete: ${success_count}/${shader_count} successful')
}
}
fn find_glslc() !string {
// Check common locations
paths := [
'/usr/bin/glslc',
'/usr/local/bin/glslc',
'/opt/homebrew/bin/glslc' // macOS
]
for path in paths {
if os.is_file(path) {
return path
}
}
// Try PATH using os.which
glslc_path := os.find_abs_path_of_executable('glslc') or { panic(err) }
if glslc_path != '' {
return glslc_path
}
return error('glslc not found. Install Vulkan SDK or shaderc')
}
fn get_object_file(source_file string, object_dir string) string {
// Replace src_dir with object_dir and change extension to .o
mut obj_file := source_file.replace('src', object_dir)
obj_file = obj_file.replace('.cpp', '.o').replace('.cc', '.o').replace('.cxx', '.o')
return obj_file
}
fn find_source_files(dir string) ![]string {
mut files := []string{}
if !os.is_dir(dir) {
return error('Source directory does not exist: ${dir}')
}
items := os.ls(dir) or { return error('Failed to list directory: ${dir}') }
for item in items {
full_path := os.join_path(dir, item)
if os.is_file(full_path) {
if item.ends_with('.cpp') || item.ends_with('.cc') || item.ends_with('.cxx') {
files << full_path
}
} else if os.is_dir(full_path) {
// Recursively search subdirectories
sub_files := find_source_files(full_path)!
files << sub_files
}
}
return files
}
fn needs_recompile(source_file string, object_file string) bool {
src_mtime := os.file_last_mod_unix(source_file)
obj_mtime := if os.is_file(object_file) {
os.file_last_mod_unix(object_file)
} else {
0
}
// Source is newer than object
if src_mtime > obj_mtime {
return true
}
// Check dependencies
dependencies := deps.extract_dependencies(source_file) or { return true }
for dep in dependencies {
if !os.is_file(dep) {
return true
}
dep_mtime := os.file_last_mod_unix(dep)
if dep_mtime > obj_mtime {
return true
}
}
return false
}