├── .github └── FUNDING.yml ├── .gitignore ├── License.txt ├── Readme.md ├── Release.bat ├── compiler.go ├── compiler_darwin.go ├── compiler_linux.go ├── compiler_windows.go ├── conan.go ├── context.go ├── fileexists.go ├── getcompiler_darwin.go ├── getcompiler_linux.go ├── getcompiler_windows.go ├── getsourcefiles.go ├── go.mod ├── go.sum ├── main.go └── package.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: openplanet 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Release/ 2 | .vscode/ 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 |

7 | 8 | ## Example 9 | Let's say you have a folder containing some source files: 10 | 11 | ```c++ 12 | // main.cpp 13 | #include "test.h" 14 | int main() { 15 | test(); 16 | return 0; 17 | } 18 | 19 | // test.h 20 | void test(); 21 | 22 | // test.cpp 23 | #include 24 | void test() { 25 | printf("Hello, world\n"); 26 | } 27 | ``` 28 | 29 | You run `qb` in this directory: 30 | 31 | ``` 32 | ~/qbtest $ qb 33 | 22:30:40.363 | main.cpp 34 | 22:30:40.364 | test.cpp 35 | 22:30:40.456 | 👏 qbtest 36 | 22:30:40.456 | ⏳ compile 53.9738ms, link 39.1138ms 37 | ``` 38 | 39 | And you run the resulting binary: 40 | ``` 41 | ~/qbtest $ ./qbtest 42 | Hello, world 43 | ``` 44 | 45 | ## Installing 46 | To install `qb`, make sure you have [Go](https://go.dev) installed, then run: 47 | ``` 48 | go install github.com/codecat/qb@master 49 | ``` 50 | 51 | ## Commands 52 | You can pass a number of commands to `qb`. 53 | 54 | ### `qb run` 55 | Runs the binary after building it. 56 | 57 | ### `qb clean` 58 | Cleans all output files that qb could generate. 59 | 60 | ## Optional configuration 61 | Since `qb` is meant to be a zero configuration tool, you don't have to do any configuration to get going quickly. It will do its best to find appropriate defaults for your setup, you just run `qb` and it builds. 62 | 63 | If you **do** want a little bit more control over what happens, you can either use command line flags or create a configuration file in your source folder. 64 | 65 | ### Command line options 66 | ``` 67 | qb [--name name] 68 | [--out dir] 69 | [--type ] 70 | [--pkg name] 71 | [--static] 72 | [--debug] 73 | [--verbose] 74 | [--strict] 75 | [--exceptions ] 76 | [--optimize ] 77 | [--cppstd ] 78 | [--cstd ] 79 | [--include ] 80 | [--define ] 81 | ``` 82 | 83 | #### `--name` 84 | Sets the name of the project and controls the output filename. You should not provide any file extension here as it will be added automatically. 85 | 86 | If no name is passed, the name of the current directory will be used. 87 | 88 | For example, `--name foo` will produce a binary `foo` on Linux, and `foo.exe` on Windows. 89 | 90 | #### `--out` 91 | Sets the output directory for the linker output. If no output directory is passed, the current directory will be used. 92 | 93 | #### `--type` 94 | Sets the type of the project, which can be an executable or a (dynamic) library. This is specified using the keywords `exe`, `dll`, or `lib`. 95 | 96 | For example, to create a dynamic library, you would pass `--type dll`. 97 | 98 | #### `--pkg` 99 | Adds a package to link to by its name. `qb` will try to resolve the package by itself, using a variety of sources. Listed here are the sources, in the order that they will be searched for: 100 | 101 | 1. **Local configuration**: If you have a `qb.toml` file, this will check for packages defined there. 102 | ```toml 103 | [package.sfml] 104 | includes = [ "D:\\Libs\\SFML-2.5.1\\include\\" ] 105 | linkdirs = [ "D:\\Libs\\SFML-2.5.1\\lib\\" ] 106 | links = [ 107 | "sfml-main.lib", 108 | "sfml-graphics-s.lib", 109 | "sfml-system-s.lib", 110 | "sfml-window-s.lib", 111 | "opengl32.lib", 112 | "winmm.lib", 113 | "gdi32.lib", 114 | ] 115 | defines = [ "SFML_STATIC" ] 116 | ``` 117 | 2. **pkgconfig**: If you have `pkg-config` installed on your system, it will be checking for packages from there. 118 | 3. Nothing else yet, but the following is planned: global configuration (like local, but system-wide), and vcpkg (for Windows). 119 | 120 | For example, to link with SFML, we can add `--pkg sfml`, as long as `sfml` can be resolved by one of the package sources. 121 | 122 | Additionally, if [Conan](https://conan.io/) is installed, it may be used as a way to manage packages. If a `conanfile.txt` exists, it will run `conan install .` (unless `conanbuildfile.txt` already exists). Then `conanbuildfile.txt` is used to properly compile & link to any dependencies in the Conanfile. 123 | 124 | #### `--static` 125 | Links statically in order to create a standalone binary that does not perform any loading of dynamic libraries. 126 | 127 | #### `--debug` 128 | Produces debug information for the resulting binary. On Windows that means a `.pdb` file, on Linux that means embedding debug information into the binary itself so that it can be used with gdb, and on Mac that means a `.dSYM` bundle. 129 | 130 | #### `--verbose` 131 | Makes it so that all compiler and linker commands will be printed to the log. Useful for debugging `qb` itself. 132 | 133 | #### `--strict` 134 | Makes the compiler more strict with its warnings. 135 | 136 | #### `--exceptions` 137 | Sets the way that the compiler's runtime will handle exceptions. Can either be `standard` (`std`), `all`, or `minimal` (`min`). The default is `standard`. 138 | 139 | This only makes a difference on Windows, where setting this to `all` will allow the runtime to catch certain access violation and other exceptions. When it's `minimal` or `min`, the minimal amount of exception handling will be done, which is similar to `all`, but there is no stack unwinding. 140 | 141 | #### `--optimize` 142 | Sets whether to use optimization. Can either be `default`, `none`, `size`, or `speed`. The default is `default`. 143 | 144 | When this option is set to `default`, whether the binary will be optimized is defined by whether it's a debug build or not. For example, when building with `qb --debug`, you will get an unoptimized binary, but by building without any options (by just running `qb`) it will produce an optimized build. 145 | 146 | #### `--cppstd` 147 | Sets which C++ standard to use. Can either be `latest`, `20`, `17`, or `14`. The default is `latest`. 148 | 149 | #### `--cstd` 150 | Sets which C standard to use. Can either be `latest`, `17`, or `11`. The default is `latest`. 151 | 152 | #### `--include` 153 | Adds a directory to the include path. For example, to add the folders `foo` and `bar` to the include path, you would run `qb --include foo --include bar`. 154 | 155 | #### `--define` 156 | Adds a precompiler definition. For example, to define `FOO` and `BAR` in the preprocessor when compiling, you would run `qb --define FOO --define BAR`. 157 | 158 | ### Configuration file 159 | It's possible to create a `qb.toml` file (in the folder you're running `qb`) to specify your configuration options as well. This is handy if you build a lot but don't want to pass the command line options every time. 160 | 161 | The configuration file works the same as the command line options, except they are in a toml file. Values not defined in the file will remain as their defaults. For example, to make `qb` always build as a dynamic library with the name `libfoo`, you would put this in `qb.toml`: 162 | 163 | ```toml 164 | name = "libfoo" 165 | type = "dll" 166 | ``` 167 | 168 | To make a statically linked debug binary, you can put this in the configuration file: 169 | 170 | ```toml 171 | static = true 172 | debug = true 173 | ``` 174 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/codecat/go-libs/log" 14 | ) 15 | 16 | type windowsCompiler struct { 17 | installDir string 18 | installVersion string 19 | 20 | sdkDir string 21 | sdkVersion string 22 | } 23 | 24 | func (ci windowsCompiler) toolsDir() string { 25 | return filepath.Join(ci.installDir, "VC\\Tools\\MSVC", ci.installVersion) 26 | } 27 | 28 | func (ci windowsCompiler) binDir() string { 29 | return filepath.Join(ci.toolsDir(), "bin\\Hostx64\\x64") 30 | } 31 | 32 | func (ci windowsCompiler) sdkIncludeDir() string { 33 | return filepath.Join(ci.sdkDir, "include", ci.sdkVersion+".0") 34 | } 35 | 36 | func (ci windowsCompiler) sdkLibDir() string { 37 | return filepath.Join(ci.sdkDir, "lib", ci.sdkVersion+".0") 38 | } 39 | 40 | func (ci windowsCompiler) compiler() string { 41 | return filepath.Join(ci.binDir(), "cl.exe") 42 | } 43 | 44 | func (ci windowsCompiler) linker() string { 45 | return filepath.Join(ci.binDir(), "link.exe") 46 | } 47 | 48 | func (ci windowsCompiler) libber() string { 49 | return filepath.Join(ci.binDir(), "lib.exe") 50 | } 51 | 52 | func (ci windowsCompiler) includeDirs() []string { 53 | ret := make([]string, 0) 54 | 55 | // MSVC includes 56 | ret = append(ret, filepath.Join(ci.toolsDir(), "ATLMFC\\include")) 57 | ret = append(ret, filepath.Join(ci.toolsDir(), "include")) 58 | 59 | // Windows Kit includes 60 | ret = append(ret, filepath.Join(ci.sdkIncludeDir(), "ucrt")) 61 | ret = append(ret, filepath.Join(ci.sdkIncludeDir(), "shared")) 62 | ret = append(ret, filepath.Join(ci.sdkIncludeDir(), "um")) 63 | ret = append(ret, filepath.Join(ci.sdkIncludeDir(), "winrt")) 64 | ret = append(ret, filepath.Join(ci.sdkIncludeDir(), "cppwinrt")) 65 | 66 | return ret 67 | } 68 | 69 | func (ci windowsCompiler) linkDirs() []string { 70 | ret := make([]string, 0) 71 | 72 | // MSVC libraries 73 | ret = append(ret, filepath.Join(ci.toolsDir(), "ATLMFC\\lib\\x64")) 74 | ret = append(ret, filepath.Join(ci.toolsDir(), "lib\\x64")) 75 | 76 | // Windows Kit libraries 77 | ret = append(ret, filepath.Join(ci.sdkLibDir(), "ucrt\\x64")) 78 | ret = append(ret, filepath.Join(ci.sdkLibDir(), "um\\x64")) 79 | 80 | return ret 81 | } 82 | 83 | func (ci windowsCompiler) Compile(path, objDir string, options *CompilerOptions) error { 84 | // cl.exe args: https://learn.microsoft.com/en-us/cpp/build/reference/compiler-options-listed-by-category?view=msvc-170 85 | 86 | fileext := filepath.Ext(path) 87 | filename := strings.TrimSuffix(filepath.Base(path), fileext) 88 | 89 | args := make([]string, 0) 90 | args = append(args, "/nologo") // Suppress startup banner 91 | args = append(args, "/c") // Compile without linking 92 | args = append(args, "/GS") // Enables buffer security checks 93 | args = append(args, "/Qspectre") // Add instructions to mitigate Spectre variant 1 security vulnerabilities 94 | args = append(args, "/Zc:inline") // Remove unreferenced function or data if it is COMDAT or has internal linkage only 95 | 96 | // Warnings: command line default is /W1, Visual Studio default is /W3 97 | if options.Strict { 98 | args = append(args, "/W4") // Note: Using /Wall enables warnings like C4710 when using functions like printf, so we leave it at /W4 99 | args = append(args, "/WX") // Treats all warnings as errors 100 | } else { 101 | args = append(args, "/W3") 102 | } 103 | 104 | // Set object output path 105 | args = append(args, fmt.Sprintf("/Fo%s\\%s.obj", objDir, filename)) 106 | 107 | // Define the runtime flag 108 | runtimeFlag := "/M" 109 | if options.Static { 110 | runtimeFlag += "T" 111 | } else { 112 | runtimeFlag += "D" 113 | } 114 | if options.Debug { 115 | runtimeFlag += "d" 116 | } 117 | args = append(args, runtimeFlag) 118 | 119 | // Add exception handling flags in C++ 120 | if fileext != ".c" { 121 | if options.Exceptions == ExceptionsStandard { 122 | args = append(args, "/EHsc") 123 | } else if options.Exceptions == ExceptionsAll { 124 | args = append(args, "/EHa") 125 | } else if options.Exceptions == ExceptionsMinimal { 126 | args = append(args, "/EH") 127 | } 128 | } 129 | 130 | // Add optimization flags 131 | if options.Optimization == OptimizeSize { 132 | args = append(args, "/O1") 133 | } else if options.Optimization == OptimizeSpeed { 134 | args = append(args, "/O2") 135 | } 136 | 137 | // Add C++ standard flag 138 | if fileext != ".c" { 139 | switch options.CPPStandard { 140 | case CPPStandardLatest: 141 | args = append(args, "/std:c++latest") 142 | case CPPStandard20: 143 | args = append(args, "/std:c++20") 144 | case CPPStandard17: 145 | args = append(args, "/std:c++17") 146 | case CPPStandard14: 147 | args = append(args, "/std:c++14") 148 | } 149 | } 150 | 151 | // Add C standard flag 152 | if fileext == ".c" { 153 | switch options.CStandard { 154 | case CStandardLatest: 155 | args = append(args, "/std:clatest") 156 | case CStandard17: 157 | args = append(args, "/std:c17") 158 | case CStandard11: 159 | args = append(args, "/std:c11") 160 | } 161 | } 162 | 163 | // Add include directories 164 | for _, dir := range options.IncludeDirectories { 165 | args = append(args, "/I"+dir) 166 | } 167 | 168 | // Add precompiler definitions 169 | for _, define := range options.Defines { 170 | args = append(args, "/D"+define) 171 | } 172 | 173 | // Add additional compiler flags for C/C++ 174 | args = append(args, options.CompilerFlagsCXX...) 175 | 176 | // Add additional compiler flags for C++ 177 | if fileext != ".c" { 178 | args = append(args, options.CompilerFlagsCPP...) 179 | } 180 | 181 | // Add additional compiler flags for C 182 | if fileext == ".c" { 183 | args = append(args, options.CompilerFlagsC...) 184 | } 185 | 186 | args = append(args, path) 187 | 188 | cmd := exec.Command(ci.compiler(), args...) 189 | cmd.Env = append(os.Environ(), 190 | "INCLUDE="+strings.Join(ci.includeDirs(), ";"), 191 | ) 192 | 193 | if options.Verbose { 194 | log.Trace("%s", strings.Join(cmd.Args, " ")) 195 | } 196 | 197 | outputBytes, err := cmd.CombinedOutput() 198 | if err != nil { 199 | output := strings.Trim(string(outputBytes), "\r\n") 200 | // Skip the first line from the output as it will always be the filename 201 | lines := strings.SplitN(output, "\n", 2) 202 | return errors.New(lines[1]) 203 | } 204 | return nil 205 | } 206 | 207 | func (ci windowsCompiler) Link(objDir, outPath string, outType LinkType, options *CompilerOptions) (string, error) { 208 | // link.exe args: https://learn.microsoft.com/en-us/cpp/build/reference/linker-options?view=msvc-170 209 | 210 | exeName := ci.linker() 211 | 212 | args := make([]string, 0) 213 | args = append(args, "/nologo") 214 | args = append(args, "/machine:x64") 215 | args = append(args, "/incremental:no") 216 | 217 | if options.Debug { 218 | args = append(args, "/debug") 219 | } 220 | 221 | switch outType { 222 | case LinkExe: 223 | outPath += ".exe" 224 | 225 | case LinkDll: 226 | outPath += ".dll" 227 | args = append(args, "/dll") 228 | 229 | case LinkLib: 230 | exeName = ci.libber() 231 | outPath += ".lib" 232 | args = append(args, "/lib") 233 | } 234 | 235 | args = append(args, "/out:"+outPath) 236 | 237 | // Add additional library paths 238 | for _, dir := range options.LinkDirectories { 239 | args = append(args, "/libpath:"+dir) 240 | } 241 | 242 | // Add libraries to link 243 | args = append(args, options.LinkLibraries...) 244 | 245 | // Add additional linker flags 246 | args = append(args, options.LinkerFlags...) 247 | 248 | // Link to some common standard libraries 249 | args = append(args, "kernel32.lib") 250 | args = append(args, "user32.lib") 251 | args = append(args, "shell32.lib") 252 | args = append(args, "advapi32.lib") 253 | 254 | filepath.Walk(objDir, func(path string, info os.FileInfo, err error) error { 255 | if err != nil { 256 | return err 257 | } 258 | if info.IsDir() || !strings.HasSuffix(path, ".obj") { 259 | return nil 260 | } 261 | args = append(args, path) 262 | return nil 263 | }) 264 | 265 | cmd := exec.Command(exeName, args...) 266 | cmd.Env = append(os.Environ(), 267 | "LIB="+strings.Join(ci.linkDirs(), ";"), 268 | ) 269 | 270 | if options.Verbose { 271 | log.Trace("%s", strings.Join(cmd.Args, " ")) 272 | } 273 | 274 | outputBytes, err := cmd.CombinedOutput() 275 | if err != nil { 276 | output := strings.Trim(string(outputBytes), "\r\n") 277 | return "", errors.New(output) 278 | } 279 | return outPath, nil 280 | } 281 | 282 | func (ci windowsCompiler) Clean(name string) { 283 | os.Remove(name + ".exe") 284 | os.Remove(name + ".dll") 285 | os.Remove(name + ".lib") 286 | os.Remove(name + ".pdb") 287 | } 288 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /getcompiler_darwin.go: -------------------------------------------------------------------------------- 1 | // +build darwin 2 | 3 | package main 4 | 5 | func getCompiler() (Compiler, error) { 6 | return darwinCompiler{}, nil 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "slices" 9 | "time" 10 | 11 | "github.com/codecat/go-libs/log" 12 | "github.com/spf13/pflag" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | func hasCommand(cmd string) bool { 17 | for _, arg := range pflag.Args() { 18 | if arg == cmd { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | 25 | func main() { 26 | // Configure logging 27 | log.CurrentConfig.Category = false 28 | 29 | // Prepare possible command line flags 30 | pflag.String("name", "", "binary output name without the extension") 31 | pflag.String("out", "", "the output directory") 32 | pflag.String("type", "exe", "binary output type, either \"exe\", \"dll\", or \"lib\"") 33 | pflag.Bool("static", false, "link statically to create a standalone binary") 34 | pflag.Bool("debug", false, "produce debug information") 35 | pflag.Bool("verbose", false, "print all compiler and linker commands being executed") 36 | pflag.Bool("strict", false, "be more strict in compiler warnings") 37 | pflag.String("exceptions", "std", "way to handle exceptions, either \"std\", \"all\", or \"min\"") 38 | pflag.String("optimize", "default", "enable optimizations, either \"defualt\", \"none\", \"size\", or \"speed\"") 39 | pflag.String("cppstd", "latest", "select the C++ standard to use, either \"latest\", \"20\", \"17\", or \"14\"") 40 | pflag.String("cstd", "latest", "select the C standard to use, either \"latest\", \"17\", or \"11\"") 41 | pflag.StringSlice("include", nil, "directories to add to the include path") 42 | pflag.StringSlice("define", nil, "adds a precompiler definition") 43 | pflag.StringSlice("pkg", nil, "packages to link for compilation") 44 | pflag.Parse() 45 | 46 | // Load a qb.toml file, if it exists 47 | viper.AddConfigPath(".") 48 | viper.SetConfigName("qb") 49 | viper.BindPFlags(pflag.CommandLine) 50 | err := viper.ReadInConfig() 51 | if err == nil { 52 | log.Info("Using build configuration file %s", filepath.Base(viper.ConfigFileUsed())) 53 | } 54 | 55 | // Prepare qb's internal context 56 | ctx, err := NewContext() 57 | if err != nil { 58 | log.Fatal("Unable to initialize context: %s", err.Error()) 59 | os.Exit(1) 60 | } 61 | 62 | // Find the name of the project 63 | ctx.Name = viper.GetString("name") 64 | if ctx.Name == "" { 65 | // If there's no name set, use the name of the current directory 66 | currentDir, _ := filepath.Abs(".") 67 | ctx.Name = filepath.Base(currentDir) 68 | } 69 | 70 | // Get the output path 71 | ctx.OutPath = viper.GetString("out") 72 | 73 | // Get the link type 74 | switch viper.GetString("type") { 75 | case "exe": 76 | ctx.Type = LinkExe 77 | case "dll": 78 | ctx.Type = LinkDll 79 | case "lib": 80 | ctx.Type = LinkLib 81 | default: 82 | ctx.Type = LinkExe 83 | } 84 | 85 | // If we only have to clean, do that and exit 86 | if hasCommand("clean") { 87 | ctx.Compiler.Clean(ctx.Name) 88 | return 89 | } 90 | 91 | // Load any compiler options 92 | ctx.CompilerOptions.Static = viper.GetBool("static") 93 | ctx.CompilerOptions.Debug = viper.GetBool("debug") 94 | ctx.CompilerOptions.Verbose = viper.GetBool("verbose") 95 | ctx.CompilerOptions.Strict = viper.GetBool("strict") 96 | 97 | // Load the exceptions method 98 | exceptionsType := viper.GetString("exceptions") 99 | switch exceptionsType { 100 | case "", "std", "standard": 101 | ctx.CompilerOptions.Exceptions = ExceptionsStandard 102 | case "all": 103 | ctx.CompilerOptions.Exceptions = ExceptionsAll 104 | case "min", "minimal": 105 | ctx.CompilerOptions.Exceptions = ExceptionsMinimal 106 | default: 107 | log.Warn("Unrecognized exceptions type %s", exceptionsType) 108 | } 109 | 110 | // Load optimization options 111 | optimizeType := viper.GetString("optimize") 112 | switch optimizeType { 113 | case "", "default": 114 | ctx.CompilerOptions.Optimization = OptimizeDefault 115 | case "none": 116 | ctx.CompilerOptions.Optimization = OptimizeNone 117 | case "size": 118 | ctx.CompilerOptions.Optimization = OptimizeSize 119 | case "speed": 120 | ctx.CompilerOptions.Optimization = OptimizeSpeed 121 | default: 122 | log.Warn("Unrecognized optimization type %s", optimizeType) 123 | } 124 | 125 | // If we're on default optimization, optimize for speed if we're not a debug build 126 | if ctx.CompilerOptions.Optimization == OptimizeDefault && !ctx.CompilerOptions.Debug { 127 | ctx.CompilerOptions.Optimization = OptimizeSpeed 128 | } 129 | 130 | // Load C++ compiler standard 131 | cppStandard := viper.GetString("cppstd") 132 | switch cppStandard { 133 | case "", "latest": 134 | ctx.CompilerOptions.CPPStandard = CPPStandardLatest 135 | case "20": 136 | ctx.CompilerOptions.CPPStandard = CPPStandard20 137 | case "17": 138 | ctx.CompilerOptions.CPPStandard = CPPStandard17 139 | case "14": 140 | ctx.CompilerOptions.CPPStandard = CPPStandard14 141 | default: 142 | log.Warn("Unrecognized C++ compiler standard %s", cppStandard) 143 | } 144 | 145 | // Load C compiler standard 146 | cStandard := viper.GetString("cstd") 147 | switch cStandard { 148 | case "", "latest": 149 | ctx.CompilerOptions.CStandard = CStandardLatest 150 | case "17": 151 | ctx.CompilerOptions.CStandard = CStandard17 152 | case "11": 153 | ctx.CompilerOptions.CStandard = CStandard11 154 | default: 155 | log.Warn("Unrecognized C compiler standard %s", cStandard) 156 | } 157 | 158 | // Add custom include directories 159 | includes := viper.GetStringSlice("include") 160 | for _, include := range includes { 161 | fi, err := os.Stat(include) 162 | if err != nil { 163 | log.Warn("Unable to include directory %s: %s", include, err.Error()) 164 | continue 165 | } 166 | 167 | if !fi.IsDir() { 168 | log.Warn("Include path is not a directory: %s", include) 169 | continue 170 | } 171 | 172 | ctx.CompilerOptions.IncludeDirectories = append(ctx.CompilerOptions.IncludeDirectories, include) 173 | } 174 | 175 | // Add preprocessor definitions 176 | defines := viper.GetStringSlice("define") 177 | ctx.CompilerOptions.Defines = append(ctx.CompilerOptions.Defines, defines...) 178 | 179 | // Find packages 180 | packages := viper.GetStringSlice("pkg") 181 | for _, pkg := range packages { 182 | pkgInfo := addPackage(ctx.CompilerOptions, pkg) 183 | if pkgInfo == nil { 184 | log.Warn("Unable to find package %s!", pkg) 185 | continue 186 | } 187 | } 188 | 189 | // To support Conan: run "conan install", if a conanfile exists, but conanbuildinfo.txt does not exist 190 | if fileExists("conanfile.txt") && !fileExists("conanbuildinfo.txt") { 191 | log.Info("Conanfile found: installing dependencies from Conan") 192 | err := exec.Command("conan", "install", ".").Run() 193 | if err != nil { 194 | log.Warn("Conan install failed: %s", err.Error()) 195 | } 196 | } 197 | 198 | // To support Conan: use conanbuildinfo.txt, if it exists 199 | if fileExists("conanbuildinfo.txt") { 200 | conan, err := loadConanFile("conanbuildinfo.txt") 201 | if err != nil { 202 | log.Warn("Unable to load conanbuildinfo.txt: %s", err.Error()) 203 | } else { 204 | addConanPackages(ctx, conan) 205 | } 206 | } 207 | 208 | // Find all the source files to compile 209 | ctx.SourceFiles, err = getSourceFiles() 210 | if err != nil { 211 | log.Fatal("Unable to read directory: %s", err.Error()) 212 | os.Exit(1) 213 | } 214 | 215 | if len(ctx.SourceFiles) == 0 { 216 | log.Warn("No source files found!") 217 | os.Exit(1) 218 | } 219 | 220 | // Make a temporary folder for .obj files 221 | ctx.ObjectPath = filepath.Join(os.TempDir(), fmt.Sprintf("qb_%d", time.Now().Unix())) 222 | os.Mkdir(ctx.ObjectPath, 0777) 223 | defer os.RemoveAll(ctx.ObjectPath) 224 | 225 | // Perform the compilation 226 | timeStart := time.Now() 227 | performCompilation(ctx) 228 | timeCompilation := time.Since(timeStart) 229 | 230 | // Stop if there were any compiler errors 231 | if ctx.CompilerErrors > 0 { 232 | log.Fatal("😢 Compilation failed!") 233 | os.Exit(1) 234 | } 235 | 236 | // Perform the linking 237 | timeStart = time.Now() 238 | outPath, err := performLinking(ctx) 239 | timeLinking := time.Since(timeStart) 240 | 241 | // Stop if linking failed 242 | if err != nil { 243 | log.Fatal("😢 Link failed!") 244 | log.Fatal("%s", err.Error()) 245 | os.Exit(1) 246 | } 247 | 248 | // Report succcess 249 | log.Info("👏 %s", outPath) 250 | log.Info("⏳ compile %v, link %v", timeCompilation, timeLinking) 251 | 252 | // Run the binary if it's requested 253 | if hasCommand("run") && viper.GetString("type") == "exe" { 254 | // We have to use the absolute path here to make this work on Linux and MacOS 255 | absOutPath, _ := filepath.Abs(outPath) 256 | cmd := exec.Command(absOutPath) 257 | cmd.Args = slices.Concat([]string{absOutPath}, pflag.Args()[1:]) 258 | cmd.Stdin = os.Stdin 259 | cmd.Stdout = os.Stdout 260 | cmd.Stderr = os.Stderr 261 | cmd.Run() 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------