├── .gitignore ├── .github └── FUNDING.yml ├── getcompiler_darwin.go ├── fileexists.go ├── Release.bat ├── getsourcefiles.go ├── getcompiler_linux.go ├── License.txt ├── context.go ├── go.mod ├── getcompiler_windows.go ├── package.go ├── conan.go ├── compiler_linux.go ├── compiler_darwin.go ├── go.sum ├── compiler.go ├── Readme.md ├── compiler_windows.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | Release/ 2 | .vscode/ 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: openplanet 2 | -------------------------------------------------------------------------------- /getcompiler_darwin.go: -------------------------------------------------------------------------------- 1 | // +build darwin 2 | 3 | package main 4 | 5 | func getCompiler() (Compiler, error) { 6 | return darwinCompiler{}, nil 7 | } 8 | -------------------------------------------------------------------------------- /fileexists.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "os" 4 | 5 | func fileExists(path string) bool { 6 | info, err := os.Stat(path) 7 | if os.IsNotExist(err) { 8 | return false 9 | } 10 | return !info.IsDir() 11 | } 12 | -------------------------------------------------------------------------------- /Release.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rmdir /s /q Release 4 | 5 | set GOARCH=amd64 6 | 7 | set GOOS=windows 8 | go build -ldflags "-s" -o Release/Windows/qb.exe 9 | 10 | set GOOS=linux 11 | go build -ldflags "-s" -o Release/Linux/qb 12 | 13 | set GOOS=darwin 14 | go build -ldflags "-s" -o Release/MacOS/qb 15 | -------------------------------------------------------------------------------- /getsourcefiles.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "regexp" 7 | ) 8 | 9 | func getSourceFiles() ([]string, error) { 10 | ret := make([]string, 0) 11 | err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { 12 | if err != nil { 13 | return err 14 | } 15 | 16 | if info.IsDir() { 17 | return nil 18 | } 19 | 20 | if ok, _ := regexp.Match("\\.(cpp|c)$", []byte(path)); !ok { 21 | return nil 22 | } 23 | 24 | ret = append(ret, path) 25 | return nil 26 | }) 27 | return ret, err 28 | } 29 | -------------------------------------------------------------------------------- /getcompiler_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "os/exec" 8 | ) 9 | 10 | func getCompiler() (Compiler, error) { 11 | //TODO: Check if we have gcc and ld installed 12 | //TODO: Add option for other flavors of gcc (eg. mingw) 13 | 14 | toolset := "" 15 | 16 | if _, err := exec.LookPath("clang"); err == nil { 17 | toolset = "clang" 18 | } else if _, err := exec.LookPath("gcc"); err == nil { 19 | toolset = "gcc" 20 | } else { 21 | return nil, errors.New("couldn't find clang or gcc in the PATH") 22 | } 23 | 24 | return linuxCompiler{ 25 | toolset: toolset, 26 | }, nil 27 | } 28 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 - 2025 Melissa Geels 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Context contains all the build system states that have to be remembered. 4 | type Context struct { 5 | // Name is the name of the project. 6 | Name string 7 | 8 | // Final binary type we want to link. 9 | Type LinkType 10 | 11 | // SourceFiles contains paths to all the .c and .cpp files that have to be compiled. 12 | SourceFiles []string 13 | 14 | // ObjectPath is the intermediate folder where object files should be stored. 15 | ObjectPath string 16 | 17 | // OutPath is the directory where the final binary is written to. 18 | OutPath string 19 | 20 | // Compiler is an abstract interface used for compiling and linking on multiple platforms. 21 | Compiler Compiler 22 | CompilerErrors int 23 | CompilerOptions *CompilerOptions 24 | CompilerWorkerChannel chan CompilerWorkerTask 25 | CompilerWorkerFinished chan int 26 | } 27 | 28 | // NewContext creates a new context with initial values. 29 | func NewContext() (*Context, error) { 30 | compiler, err := getCompiler() 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return &Context{ 36 | Compiler: compiler, 37 | CompilerOptions: &CompilerOptions{}, 38 | 39 | SourceFiles: make([]string, 0), 40 | }, nil 41 | } 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/codecat/qb 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/codecat/go-libs v0.0.0-20210906174629-ffa6674c8e05 7 | github.com/mattn/go-shellwords v1.0.12 8 | github.com/spf13/pflag v1.0.5 9 | github.com/spf13/viper v1.18.2 10 | golang.org/x/sys v0.17.0 11 | ) 12 | 13 | require ( 14 | github.com/fatih/color v1.16.0 // indirect 15 | github.com/fsnotify/fsnotify v1.7.0 // indirect 16 | github.com/hashicorp/hcl v1.0.0 // indirect 17 | github.com/magiconair/properties v1.8.7 // indirect 18 | github.com/mattn/go-colorable v0.1.13 // indirect 19 | github.com/mattn/go-isatty v0.0.20 // indirect 20 | github.com/mitchellh/mapstructure v1.5.0 // indirect 21 | github.com/pelletier/go-toml/v2 v2.1.1 // indirect 22 | github.com/sagikazarmark/locafero v0.4.0 // indirect 23 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 24 | github.com/sourcegraph/conc v0.3.0 // indirect 25 | github.com/spf13/afero v1.11.0 // indirect 26 | github.com/spf13/cast v1.6.0 // indirect 27 | github.com/subosito/gotenv v1.6.0 // indirect 28 | go.uber.org/multierr v1.11.0 // indirect 29 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect 30 | golang.org/x/text v0.14.0 // indirect 31 | gopkg.in/ini.v1 v1.67.0 // indirect 32 | gopkg.in/yaml.v3 v3.0.1 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /getcompiler_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package main 4 | 5 | import ( 6 | "encoding/json" 7 | "errors" 8 | "io/ioutil" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | 13 | "golang.org/x/sys/windows/registry" 14 | ) 15 | 16 | type vswhereOutput struct { 17 | InstanceID string `json:"instanceId"` 18 | InstallPath string `json:"installationPath"` 19 | } 20 | 21 | func getCompiler() (Compiler, error) { 22 | ret := windowsCompiler{} 23 | 24 | vswherePath := "C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe" 25 | if !fileExists(vswherePath) { 26 | return nil, errors.New("couldn't find vswhere in the default path") 27 | } 28 | 29 | cmd := exec.Command(vswherePath, "-latest", "-format", "json") 30 | output, err := cmd.Output() 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | var outputs []vswhereOutput 36 | json.Unmarshal(output, &outputs) 37 | 38 | if len(outputs) == 0 { 39 | return nil, errors.New("vswhere didn't return any installations") 40 | } 41 | 42 | ret.installDir = outputs[0].InstallPath 43 | installVersionBytes, err := ioutil.ReadFile(filepath.Join(ret.installDir, "VC\\Auxiliary\\Build\\Microsoft.VCToolsVersion.default.txt")) 44 | if err != nil { 45 | return nil, errors.New("unable to open Microsoft.VCToolsVersion.default.txt") 46 | } 47 | 48 | ret.installVersion = strings.Trim(string(installVersionBytes), "\r\n") 49 | 50 | // Get Windows 10 SDK path 51 | // HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Microsoft SDKs\Windows\v10.0 52 | keySDK, err := registry.OpenKey(registry.LOCAL_MACHINE, "SOFTWARE\\WOW6432Node\\Microsoft\\Microsoft SDKs\\Windows\\v10.0", registry.QUERY_VALUE) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | ret.sdkDir, _, _ = keySDK.GetStringValue("InstallationFolder") 58 | ret.sdkVersion, _, _ = keySDK.GetStringValue("ProductVersion") 59 | 60 | return ret, nil 61 | } 62 | -------------------------------------------------------------------------------- /package.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | "runtime" 6 | "strings" 7 | 8 | "github.com/mattn/go-shellwords" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | // Package contains basic information about a library. 13 | type Package struct { 14 | Name string 15 | } 16 | 17 | func addPackage(options *CompilerOptions, name string) *Package { 18 | if ret := addPackageLocal(options, name); ret != nil { 19 | return ret 20 | } 21 | 22 | //TODO: Implement global packages 23 | 24 | if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { 25 | if ret := addPackagePkgconfig(options, name); ret != nil { 26 | return ret 27 | } 28 | } 29 | 30 | //TODO: Implement vcpkg 31 | //if runtime.GOOS == "windows" { 32 | //} 33 | 34 | return nil 35 | } 36 | 37 | func addPackageLocal(options *CompilerOptions, name string) *Package { 38 | packageInfo := viper.GetStringMap("package." + name) 39 | if len(packageInfo) == 0 { 40 | return nil 41 | } 42 | 43 | return configurePackageFromConfig(options, packageInfo, name) 44 | } 45 | 46 | func addPackagePkgconfig(options *CompilerOptions, name string) *Package { 47 | // pkg-config must be installed for this to work 48 | _, err := exec.LookPath("pkg-config") 49 | if err != nil { 50 | return nil 51 | } 52 | 53 | cmdCflags := exec.Command("pkg-config", name, "--cflags") 54 | outputCflags, err := cmdCflags.CombinedOutput() 55 | if err != nil { 56 | return nil 57 | } 58 | 59 | cmdLibs := exec.Command("pkg-config", name, "--libs") 60 | outputLibs, err := cmdLibs.CombinedOutput() 61 | if err != nil { 62 | return nil 63 | } 64 | 65 | parseCflags, _ := shellwords.Parse(strings.Trim(string(outputCflags), "\r\n")) 66 | parseLibs, _ := shellwords.Parse(strings.Trim(string(outputLibs), "\r\n")) 67 | 68 | options.CompilerFlagsCXX = append(options.CompilerFlagsCXX, parseCflags...) 69 | options.LinkerFlags = append(options.LinkerFlags, parseLibs...) 70 | 71 | return &Package{ 72 | Name: name, 73 | } 74 | } 75 | 76 | func configurePackageFromConfig(options *CompilerOptions, pkg map[string]interface{}, name string) *Package { 77 | maybeUnpack(&options.IncludeDirectories, pkg["includes"]) 78 | maybeUnpack(&options.LinkDirectories, pkg["linkdirs"]) 79 | maybeUnpack(&options.LinkLibraries, pkg["links"]) 80 | maybeUnpack(&options.Defines, pkg["defines"]) 81 | maybeUnpack(&options.CompilerFlagsCXX, pkg["cflags"]) 82 | maybeUnpack(&options.LinkerFlags, pkg["lflags"]) 83 | 84 | return &Package{ 85 | Name: name, 86 | } 87 | } 88 | 89 | func maybeUnpack(dest *[]string, src interface{}) { 90 | if src == nil { 91 | return 92 | } 93 | 94 | for _, val := range src.([]interface{}) { 95 | (*dest) = append(*dest, val.(string)) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /conan.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/codecat/go-libs/log" 12 | ) 13 | 14 | type Conanfile map[string][]string 15 | 16 | func loadConanFile(path string) (Conanfile, error) { 17 | f, err := os.Open(path) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | regexHeader := regexp.MustCompile(`^\[(.*)\]$`) 23 | currentHeader := "" 24 | 25 | ret := make(Conanfile) 26 | 27 | scanner := bufio.NewScanner(f) 28 | for scanner.Scan() { 29 | line := scanner.Text() 30 | 31 | if line == "" { 32 | continue 33 | } 34 | 35 | res := regexHeader.FindStringSubmatch(line) 36 | if len(res) == 0 { 37 | if currentHeader == "" { 38 | return nil, fmt.Errorf("invalid conanfile on line: \"%s\"", line) 39 | } 40 | ret[currentHeader] = append(ret[currentHeader], strings.Trim(line, " \t")) 41 | } else { 42 | currentHeader = res[1] 43 | } 44 | } 45 | 46 | return ret, nil 47 | } 48 | 49 | func addConanPackages(ctx *Context, conan Conanfile) { 50 | //TODO: Implement all Conan features 51 | // conan["frameworkdirs"] // contains .framework files, only needed on MacOS 52 | // conan["frameworks"] // frameworks to link to 53 | // conan["bindirs"] // contains .dll files, only needed when linking with shared libraries 54 | 55 | log.Info("Adding Conan packages") 56 | 57 | isWindows := runtime.GOOS == "windows" 58 | 59 | // contains .h files 60 | ctx.CompilerOptions.IncludeDirectories = append(ctx.CompilerOptions.IncludeDirectories, conan["includedirs"]...) 61 | 62 | // contains .lib files 63 | ctx.CompilerOptions.LinkDirectories = append(ctx.CompilerOptions.LinkDirectories, conan["libdirs"]...) 64 | 65 | // libraries to link to 66 | for _, lib := range conan["libs"] { 67 | if isWindows && !strings.HasSuffix(lib, ".lib") { 68 | lib += ".lib" 69 | } 70 | ctx.CompilerOptions.LinkLibraries = append(ctx.CompilerOptions.LinkLibraries, lib) 71 | } 72 | 73 | // additional system libraries to link to 74 | for _, lib := range conan["system_libs"] { 75 | if isWindows && !strings.HasSuffix(lib, ".lib") { 76 | lib += ".lib" 77 | } 78 | ctx.CompilerOptions.LinkLibraries = append(ctx.CompilerOptions.LinkLibraries, lib) 79 | } 80 | 81 | // precompiler defines to add 82 | ctx.CompilerOptions.Defines = append(ctx.CompilerOptions.Defines, conan["defines"]...) 83 | 84 | // C++ compiler flags to add 85 | ctx.CompilerOptions.CompilerFlagsCPP = append(ctx.CompilerOptions.CompilerFlagsCPP, conan["cppflags"]...) 86 | 87 | // C/C++ compiler flags to add 88 | ctx.CompilerOptions.CompilerFlagsCXX = append(ctx.CompilerOptions.CompilerFlagsCXX, conan["cxxflags"]...) 89 | 90 | // C compiler flags to add 91 | ctx.CompilerOptions.CompilerFlagsC = append(ctx.CompilerOptions.CompilerFlagsC, conan["cflags"]...) 92 | 93 | if ctx.Type == LinkDll { 94 | // linker flags to add when building a shared library 95 | ctx.CompilerOptions.LinkerFlags = append(ctx.CompilerOptions.LinkerFlags, conan["sharedlinkflags"]...) 96 | 97 | } else if ctx.Type == LinkExe { 98 | // linker flags to add when building an executable 99 | ctx.CompilerOptions.LinkerFlags = append(ctx.CompilerOptions.LinkerFlags, conan["exelinkflags"]...) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /compiler_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/codecat/go-libs/log" 13 | ) 14 | 15 | type linuxCompiler struct { 16 | toolset string 17 | } 18 | 19 | func (ci linuxCompiler) Compile(path, objDir string, options *CompilerOptions) error { 20 | fileext := filepath.Ext(path) 21 | filename := strings.TrimSuffix(filepath.Base(path), fileext) 22 | 23 | args := make([]string, 0) 24 | args = append(args, "-c") 25 | args = append(args, "-o", filepath.Join(objDir, filename+".o")) 26 | args = append(args, "-std=c++17") 27 | 28 | // Set warnings flags 29 | if options.Strict { 30 | args = append(args, "-Wall") 31 | args = append(args, "-Wextra") 32 | args = append(args, "-Werror") 33 | } 34 | 35 | // Set debug flag 36 | if options.Debug { 37 | args = append(args, "-g") 38 | } 39 | 40 | // Add optimization flags 41 | if options.Optimization == OptimizeSize { 42 | args = append(args, "-Os") 43 | } else if options.Optimization == OptimizeSpeed { 44 | args = append(args, "-O3") 45 | } 46 | 47 | // Add C++ standard flag 48 | if fileext != ".c" { 49 | switch options.CPPStandard { 50 | case CPPStandardLatest: 51 | args = append(args, "-std=c++23") 52 | case CPPStandard20: 53 | args = append(args, "-std=c++20") 54 | case CPPStandard17: 55 | args = append(args, "-std=c++17") 56 | case CPPStandard14: 57 | args = append(args, "-std=c++14") 58 | } 59 | } 60 | 61 | // Add C standard flag 62 | if fileext == ".c" { 63 | switch options.CStandard { 64 | case CStandardLatest: 65 | args = append(args, "-std=c2x") 66 | case CStandard17: 67 | args = append(args, "-std=c17") 68 | case CStandard11: 69 | args = append(args, "-std=c11") 70 | } 71 | } 72 | 73 | // Add include directories 74 | for _, dir := range options.IncludeDirectories { 75 | args = append(args, "-I"+dir) 76 | } 77 | 78 | // Add precompiler definitions 79 | for _, define := range options.Defines { 80 | args = append(args, "-D"+define) 81 | } 82 | 83 | // Add additional compiler flags for C/C++ 84 | args = append(args, options.CompilerFlagsCXX...) 85 | 86 | // Add additional compiler flags for C++ 87 | if fileext != ".c" { 88 | args = append(args, options.CompilerFlagsCPP...) 89 | } 90 | 91 | // Add additional compiler flags for C 92 | if fileext == ".c" { 93 | args = append(args, options.CompilerFlagsC...) 94 | } 95 | 96 | args = append(args, path) 97 | 98 | cmd := exec.Command(ci.toolset, args...) 99 | 100 | if options.Verbose { 101 | log.Trace("%s", strings.Join(cmd.Args, " ")) 102 | } 103 | 104 | outputBytes, err := cmd.CombinedOutput() 105 | if err != nil { 106 | output := strings.Trim(string(outputBytes), "\r\n") 107 | return errors.New(output) 108 | } 109 | return nil 110 | } 111 | 112 | func (ci linuxCompiler) Link(objDir, outPath string, outType LinkType, options *CompilerOptions) (string, error) { 113 | args := make([]string, 0) 114 | 115 | exeName := ci.toolset 116 | 117 | switch outType { 118 | case LinkExe: 119 | case LinkDll: 120 | outPath += ".so" 121 | args = append(args, "-shared") 122 | case LinkLib: 123 | exeName = "ar" 124 | outPath += ".a" 125 | } 126 | 127 | if outType == LinkLib { 128 | // r = insert with replacement 129 | // c = create new archie 130 | // s = write an index 131 | args = append(args, "rcs") 132 | args = append(args, outPath) 133 | 134 | } else { 135 | args = append(args, "-o", outPath) 136 | 137 | if ci.toolset == "gcc" { 138 | args = append(args, "-static-libgcc") 139 | args = append(args, "-static-libstdc++") 140 | } 141 | 142 | if options.Static { 143 | args = append(args, "-static") 144 | } 145 | 146 | // Add additional library paths 147 | for _, dir := range options.LinkDirectories { 148 | args = append(args, "-L"+dir) 149 | } 150 | } 151 | 152 | filepath.Walk(objDir, func(path string, info os.FileInfo, err error) error { 153 | if err != nil { 154 | return err 155 | } 156 | if info.IsDir() || !strings.HasSuffix(path, ".o") { 157 | return nil 158 | } 159 | args = append(args, path) 160 | return nil 161 | }) 162 | 163 | if outType != LinkLib { 164 | // Link to some common standard libraries 165 | args = append(args, "-lstdc++") 166 | 167 | // Add libraries to link 168 | for _, link := range options.LinkLibraries { 169 | args = append(args, "-l"+link) 170 | } 171 | 172 | // Add additional linker flags 173 | args = append(args, options.LinkerFlags...) 174 | } 175 | 176 | cmd := exec.Command(exeName, args...) 177 | 178 | if options.Verbose { 179 | log.Trace("%s", strings.Join(cmd.Args, " ")) 180 | } 181 | 182 | outputBytes, err := cmd.CombinedOutput() 183 | if err != nil { 184 | output := strings.Trim(string(outputBytes), "\r\n") 185 | return "", errors.New(output) 186 | } 187 | return outPath, nil 188 | } 189 | 190 | func (ci linuxCompiler) Clean(name string) { 191 | os.Remove(name) 192 | os.Remove(name + ".so") 193 | os.Remove(name + ".a") 194 | } 195 | -------------------------------------------------------------------------------- /compiler_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/codecat/go-libs/log" 13 | ) 14 | 15 | type darwinCompiler struct { 16 | } 17 | 18 | func (ci darwinCompiler) Compile(path, objDir string, options *CompilerOptions) error { 19 | fileext := filepath.Ext(path) 20 | filename := strings.TrimSuffix(filepath.Base(path), fileext) 21 | 22 | args := make([]string, 0) 23 | args = append(args, "-c") 24 | args = append(args, "-o", filepath.Join(objDir, filename+".o")) 25 | args = append(args, "-std=c++17") // c++2a 26 | 27 | // Set warnings flags 28 | if options.Strict { 29 | args = append(args, "-Wall") 30 | args = append(args, "-Wextra") 31 | args = append(args, "-Werror") 32 | } 33 | 34 | // Set debug flag 35 | if options.Debug { 36 | args = append(args, "-g") 37 | } 38 | 39 | // Add optimization flags 40 | if options.Optimization == OptimizeSize { 41 | args = append(args, "-Os") 42 | } else if options.Optimization == OptimizeSpeed { 43 | args = append(args, "-O3") 44 | } 45 | 46 | // Add C++ standard flag 47 | if fileext != ".c" { 48 | switch options.CPPStandard { 49 | case CPPStandardLatest: 50 | args = append(args, "-std=c++2b") 51 | case CPPStandard20: 52 | args = append(args, "-std=c++20") 53 | case CPPStandard17: 54 | args = append(args, "-std=c++17") 55 | case CPPStandard14: 56 | args = append(args, "-std=c++14") 57 | } 58 | } 59 | 60 | // Add C standard flag 61 | if fileext == ".c" { 62 | switch options.CStandard { 63 | case CStandardLatest: 64 | args = append(args, "-std=c2x") 65 | case CStandard17: 66 | args = append(args, "-std=c17") 67 | case CStandard11: 68 | args = append(args, "-std=c11") 69 | } 70 | } 71 | 72 | // Add include directories 73 | for _, dir := range options.IncludeDirectories { 74 | args = append(args, "-I"+dir) 75 | } 76 | 77 | // Add precompiler definitions 78 | for _, define := range options.Defines { 79 | args = append(args, "-D"+define) 80 | } 81 | 82 | // Add additional compiler flags for C/C++ 83 | args = append(args, options.CompilerFlagsCXX...) 84 | 85 | // Add additional compiler flags for C++ 86 | if fileext != ".c" { 87 | args = append(args, options.CompilerFlagsCPP...) 88 | } 89 | 90 | // Add additional compiler flags for C 91 | if fileext == ".c" { 92 | args = append(args, options.CompilerFlagsC...) 93 | } 94 | 95 | args = append(args, path) 96 | 97 | cmd := exec.Command("clang", args...) 98 | 99 | if options.Verbose { 100 | log.Trace("%s", strings.Join(cmd.Args, " ")) 101 | } 102 | 103 | outputBytes, err := cmd.CombinedOutput() 104 | if err != nil { 105 | output := strings.Trim(string(outputBytes), "\r\n") 106 | return errors.New(output) 107 | } 108 | return nil 109 | } 110 | 111 | func (ci darwinCompiler) Link(objDir, outPath string, outType LinkType, options *CompilerOptions) (string, error) { 112 | args := make([]string, 0) 113 | 114 | exeName := "clang" 115 | 116 | switch outType { 117 | case LinkExe: 118 | case LinkDll: 119 | outPath += ".dylib" 120 | args = append(args, "-dynamiclib") 121 | case LinkLib: 122 | exeName = "ar" 123 | outPath += ".a" 124 | } 125 | 126 | if outType == LinkLib { 127 | // r = insert with replacement 128 | // c = create new archie 129 | // s = write an index 130 | args = append(args, "rcs") 131 | args = append(args, outPath) 132 | 133 | } else { 134 | args = append(args, "-o", outPath) 135 | 136 | if options.Static { 137 | args = append(args, "-static") 138 | log.Warn("Static linking is not supported on MacOS!") 139 | } 140 | 141 | // Add additional library paths 142 | for _, dir := range options.LinkDirectories { 143 | args = append(args, "-L"+dir) 144 | } 145 | 146 | // Link to some common standard libraries 147 | args = append(args, "-lstdc++") 148 | 149 | // Add libraries to link 150 | for _, link := range options.LinkLibraries { 151 | args = append(args, "-l"+link) 152 | } 153 | 154 | // Add additional linker flags 155 | args = append(args, options.LinkerFlags...) 156 | } 157 | 158 | filepath.Walk(objDir, func(path string, info os.FileInfo, err error) error { 159 | if err != nil { 160 | return err 161 | } 162 | if info.IsDir() || !strings.HasSuffix(path, ".o") { 163 | return nil 164 | } 165 | args = append(args, path) 166 | return nil 167 | }) 168 | 169 | cmd := exec.Command(exeName, args...) 170 | 171 | if options.Verbose { 172 | log.Trace("%s", strings.Join(cmd.Args, " ")) 173 | } 174 | 175 | outputBytes, err := cmd.CombinedOutput() 176 | if err != nil { 177 | output := strings.Trim(string(outputBytes), "\r\n") 178 | return "", errors.New(output) 179 | } 180 | 181 | if options.Debug { 182 | cmd = exec.Command("dsymutil", outPath) 183 | err := cmd.Run() 184 | if err != nil { 185 | log.Warn("Unable to generate debug information: %s", err.Error()) 186 | } 187 | } 188 | 189 | return outPath, nil 190 | } 191 | 192 | func (ci darwinCompiler) Clean(name string) { 193 | os.Remove(name) 194 | os.Remove(name + ".dylib") 195 | os.Remove(name + ".a") 196 | os.RemoveAll(name + ".dSYM") 197 | } 198 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/codecat/go-libs v0.0.0-20210906174629-ffa6674c8e05 h1:JSfDXHJvrIpQ8Agy//yoIlGpfIprTCDUytmf68fd/Lc= 2 | github.com/codecat/go-libs v0.0.0-20210906174629-ffa6674c8e05/go.mod h1:xJW98cHEb+Kbuu0qmoKzExh3blthZqojIYOFo27VgvE= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 6 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 7 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 8 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 9 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 10 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 11 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 12 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 13 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 14 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 15 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 16 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 17 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 18 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 19 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 20 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 21 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 22 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 23 | github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= 24 | github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= 25 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 26 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 27 | github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= 28 | github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 29 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 30 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 31 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 32 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 33 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 34 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 35 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 36 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 37 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 38 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 39 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 40 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 41 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 42 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 43 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 44 | github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= 45 | github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= 46 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 47 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 48 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 49 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 50 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 51 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 52 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 53 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 54 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 55 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 56 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 57 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= 58 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= 59 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= 62 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 63 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 64 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 65 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 66 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 67 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 68 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 69 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 70 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 71 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 72 | -------------------------------------------------------------------------------- /compiler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "path/filepath" 7 | "runtime" 8 | "strings" 9 | 10 | "github.com/codecat/go-libs/log" 11 | ) 12 | 13 | // LinkType specifies the output build type. 14 | type LinkType int 15 | 16 | const ( 17 | // LinkExe will create an executable application. Will add the ".exe" suffix on Windows. 18 | LinkExe LinkType = iota 19 | 20 | // LinkDll will create a dynamic library. Will add the ".dll" suffix on Windows, and the ".so" suffix on Linux. 21 | LinkDll 22 | 23 | // LinkLib will create a static library. Will add the ".lib" suffix on Windows, and the ".a" suffix on Linux. 24 | LinkLib 25 | ) 26 | 27 | // Compiler contains information about the compiler. 28 | type Compiler interface { 29 | Compile(path, objDir string, options *CompilerOptions) error 30 | Link(objDir, outPath string, outType LinkType, options *CompilerOptions) (string, error) 31 | Clean(name string) 32 | } 33 | 34 | // ExceptionType is the way that a compiler's runtime might handle exceptions. 35 | type ExceptionType int 36 | 37 | const ( 38 | // ExceptionsStandard is the standard way of handling exceptions, and will perform stack unwinding. 39 | ExceptionsStandard ExceptionType = iota 40 | 41 | // ExceptionsAll is only supported on Windows, and will allow catching excptions such as access violations and integer divide by zero exceptions. 42 | ExceptionsAll 43 | 44 | // ExceptionsMinimal is only supported on Windows, and is similar to ExceptionsAll, except there is no stack unwinding. 45 | ExceptionsMinimal 46 | ) 47 | 48 | // OptimizeType defines the compiler optimization type. 49 | type OptimizeType int 50 | 51 | const ( 52 | // OptimizeDefault favors speed for release builds, and disables optimization for debug builds. 53 | OptimizeDefault OptimizeType = iota 54 | 55 | // OptimizeNone performs no optimization at all. 56 | OptimizeNone 57 | 58 | // OptimizeSize favors size over speed in optimization. 59 | OptimizeSize 60 | 61 | // OptimizeSpeed favors sped over size in optimization. 62 | OptimizeSpeed 63 | ) 64 | 65 | // CPPStandardType defines the C++ standard to use. 66 | type CPPStandardType int 67 | 68 | const ( 69 | // CPPStandardLatest uses the latest C++ standard available to the compiler. 70 | CPPStandardLatest CPPStandardType = iota 71 | 72 | // CPPStandard20 uses the C++20 standard. 73 | CPPStandard20 74 | 75 | // CPPStandard17 uses the C++17 standard. 76 | CPPStandard17 77 | 78 | // CPPStandard14 uses the C++14 standard. 79 | CPPStandard14 80 | ) 81 | 82 | // CStandardType defines the C standard to use. 83 | type CStandardType int 84 | 85 | const ( 86 | // CStandardLatest uses the latest C standard available to the compiler. 87 | CStandardLatest CStandardType = iota 88 | 89 | // CStandard11 uses the C17 standard. 90 | CStandard17 91 | 92 | // CStandard11 uses the C11 standard. 93 | CStandard11 94 | ) 95 | 96 | // CompilerOptions contains options used for compiling and linking. 97 | type CompilerOptions struct { 98 | // Static sets whether to build a completely-static binary (eg. no dynamic link libraries are loaded from disk). 99 | Static bool 100 | 101 | // Debug configurations will add debug symbols. This will create a pdb file on Windows, and embed debugging information on Linux. 102 | Debug bool 103 | 104 | // Verbose compiling means we'll print the actual compiler and linker commands being executed. 105 | Verbose bool 106 | 107 | // Strict sets whether to be more strict on warnings. 108 | Strict bool 109 | 110 | // Include paths and library links 111 | IncludeDirectories []string 112 | LinkDirectories []string 113 | LinkLibraries []string 114 | 115 | // Additional compiler defines 116 | Defines []string 117 | 118 | // Additional compiler and linker flags 119 | CompilerFlagsCXX []string 120 | CompilerFlagsCPP []string 121 | CompilerFlagsC []string 122 | LinkerFlags []string 123 | 124 | // Specific options 125 | Exceptions ExceptionType 126 | Optimization OptimizeType 127 | CPPStandard CPPStandardType 128 | CStandard CStandardType 129 | } 130 | 131 | // CompilerWorkerTask describes a task for the compiler worker 132 | type CompilerWorkerTask struct { 133 | path string 134 | outputDir string 135 | } 136 | 137 | func compileWorker(ctx *Context, num int) { 138 | for { 139 | // Get a task 140 | task, ok := <-ctx.CompilerWorkerChannel 141 | if !ok { 142 | break 143 | } 144 | 145 | // Log the file we're currently compiling 146 | fileForward := strings.Replace(task.path, "\\", "/", -1) 147 | log.Info("%s", fileForward) 148 | 149 | // Invoke the compiler 150 | err := ctx.Compiler.Compile(task.path, task.outputDir, ctx.CompilerOptions) 151 | if err != nil { 152 | log.Error("Failed to compile %s!\n%s", fileForward, err.Error()) 153 | ctx.CompilerErrors++ 154 | } 155 | } 156 | 157 | ctx.CompilerWorkerFinished <- num 158 | } 159 | 160 | func performCompilation(ctx *Context) { 161 | // Prepare worker channels 162 | ctx.CompilerWorkerChannel = make(chan CompilerWorkerTask) 163 | ctx.CompilerWorkerFinished = make(chan int) 164 | 165 | // Start compiler worker routines 166 | numWorkers := runtime.NumCPU() 167 | if len(ctx.SourceFiles) < numWorkers { 168 | numWorkers = len(ctx.SourceFiles) 169 | } 170 | for i := 0; i < numWorkers; i++ { 171 | go compileWorker(ctx, i) 172 | } 173 | 174 | // Compile all the source files 175 | for _, file := range ctx.SourceFiles { 176 | // The output dir will be a sub-folder in the object directory 177 | dir := filepath.Dir(file) 178 | outputDir := filepath.Join(ctx.ObjectPath, dir) 179 | 180 | err := os.MkdirAll(outputDir, 0777) 181 | if err != nil { 182 | log.Error("Unable to create output directory %s: %s", outputDir, err.Error()) 183 | ctx.CompilerErrors++ 184 | continue 185 | } 186 | 187 | // Send the task to an available worker 188 | ctx.CompilerWorkerChannel <- CompilerWorkerTask{ 189 | path: file, 190 | outputDir: outputDir, 191 | } 192 | } 193 | 194 | // Close the worker channel 195 | close(ctx.CompilerWorkerChannel) 196 | 197 | // Wait for all workers to finish compiling 198 | for i := 0; i < numWorkers; i++ { 199 | <-ctx.CompilerWorkerFinished 200 | } 201 | } 202 | 203 | func performLinking(ctx *Context) (string, error) { 204 | // Invoke the linker 205 | return ctx.Compiler.Link(ctx.ObjectPath, path.Join(ctx.OutPath, ctx.Name), ctx.Type, ctx.CompilerOptions) 206 | } 207 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Quickbuild (qb) 2 | `qb` is a zero-configuration build system to very quickly build C/C++ projects on Linux, Windows, and MacOS. 3 | 4 |
5 |
6 |