# CC Precompiler [![Hex.pm](https://img.shields.io/hexpm/v/cc_precompiler.svg?style=flat&color=blue)](https://hex.pm/packages/cc_precompiler) C/C++ Cross-compiler Precompiler is a library that supports [elixir_make](https://github.com/elixir-lang/elixir_make)'s precompilation feature. It's customisble and easy to extend. The guide for how to `cc_precompiler` can be found in the `PRECOMPILATION_GUIED.md` file. ## Installation If [available in Hex](https://hex.pm/docs/publish), the package can be installed by adding `cc_precompiler` to your list of dependencies in `mix.exs`: ```elixir def deps do [ {:cc_precompiler, "~> 0.1.6"} ] end ``` Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) and published on [HexDocs](https://hexdocs.pm). Once published, the docs can be found at . ## Default Targets By default, it will probe some well-known C/C++ crosss-compilers existing on your system: #### Linux | Target Triplet | Compiler Prefix, `prefix` | `CC` | `CXX` | |:------------------------|:--------------------------|:----------------|:----------------| | `x86_64-linux-gnu` | `x86_64-linux-gnu-` | `#{prefix}gcc` | `#{prefix}g++` | | `i686-linux-gnu` | `i686-linux-gnu-` | `#{prefix}gcc` | `#{prefix}g++` | | `aarch64-linux-gnu` | `aarch64-linux-gnu-` | `#{prefix}gcc` | `#{prefix}g++` | | `armv7l-linux-gnuabihf` | `arm-linux-gnueabihf-` | `#{prefix}gcc` | `#{prefix}g++` | | `riscv64-linux-gnu` | `riscv64-linux-gnu-` | `#{prefix}gcc` | `#{prefix}g++` | | `powerpc64le-linux-gnu` | `powerpc64le-linux-gnu-` | `#{prefix}gcc` | `#{prefix}g++` | | `s390x-linux-gnu` | `s390x-linux-gnu-` | `#{prefix}gcc` | `#{prefix}g++` | `cc_precompiler` will try to find `#{prefix}gcc` in `$PATH`, and if `#{prefix}gcc` can be found, then the correspondong target will be activiated. Otherwise, that target will be ignored. #### macOS | Target Triplet | Compiler Prefix, `prefix` | `CC` | `CXX` | |:------------------------|:--------------------------|:-------------------|:-------------------| | `x86_64-apple-darwin` | N/A | `gcc -arch x86_64` | `g++ -arch x86_64` | | `aarch64-apple-darwin` | N/A | `gcc -arch arm64` | `g++ -arch arm64` | `cc_precompiler` will try to find `gcc` in `$PATH`, and if `gcc` can be found, then both `x86_64` and `arm64` target will be activiated. Otherwise, both targets will be ignored. #### Note Triplet for current host will be always available, `:erlang.system_info(:system_architecture)`. For macOS targets, the version part will be trimmed, e.g., `x86_64-apple-darwin21.6.0` will be `x86_64-apple-darwin`. ### Note #### Conditionally switch on/off compilation flags depending on the target During the compilation, `cc_precompiler` will set and update the environment variable `CC_PRECOMPILER_CURRENT_TARGET` to the current target's triplet. The reason we might need this is that some 3rd party library may support some feature, like AVX, but they do not offer an auto-detection mechanism, and we have to manually switch on/off corresponding compilation flags. An example with further explanation can be found on [cocoa-xu/nif_opt_flags](https://github.com/cocoa-xu/nif_opt_flags). Last but not least, as the name suggests, this environment variable is set by `cc_precompiler`, thus if you switch to another precompiler, please check their manual for the equvilent. ### Customise Precompilation Targets #### Quick Start To add custom targets in addition to the default configuration, you can set `:include_default_ones` in `project.cc_precompiler.compilers`. Default (cross-)compiler will be included if it's `true`, otherwise only specified targets will be used. Default value of `:include_default_ones` is `false` to avoid breaking changes. If a custom target has the same name as a default one, then the custom one will override the default configuration for that target (e.g., the `x86_64-linux-gnu` entry below will override the default gcc configuration and use clang instead). ```elixir def project do [ # ... cc_precompiler: [ compilers: %{ {:unix, :linux} => %{ :include_default_ones => true, "my-custom-target" => { "my-custom-target-gcc", "my-custom-target-g++" }, "x86_64-linux-gnu" => { "x86_64-linux-gnu-clang", "x86_64-linux-gnu-clang++" } } } ] ] end ``` #### Fully Customise Precompilation Targets ```elixir def project do [ # ... cc_precompiler: [ # optional config key # false - target triplet for the current machine will be included in all available targets # true - only targets listed in `compilers` will be included in all available targets # defaults to `false` only_listed_targets: true, # optional config key # this option is valid if and only if `only_listed_targets` is set to `true` # - when `exclude_current_target` is `true`, it excludes current target (i.e., the machine that builds these binaries) # from the list. This can be helpful when you're doing some complex cross-compilations, # e.g., you'd like to specify which CI job should build for the x86_64-linux-gnu target # this will force current target to be excluded from the list exclude_current_target: false, # optional config key # clean up the priv directory between different targets # # for example, common assets for different targets can stay # in the `priv` directory (instead of copying/downloading them # multiple times) # but target specific assets or .o files should be cleaned # so that `make` can compile/generate these files for the next target # # the value for `cleanup` should be a string indicating the cleanup target # in the makefile. # # for example, cc_precompiler will call `make mycleanup` between each build # if the value for the key `cleanup` is set to `mycleanup` # # also, cc_precompiler will stop if `make mycleanup` exited with non-zero code # # the default value for this key is `nil`, and in such case, cc_precompiler # will not do anything between each build cleanup: "mycleanup", # optional config key # true - the corresponding target will be available as long as we can detect either `CC` or `CXX` # false - both `CC` and `CXX` should be present on the system # defaults to `false` allow_missing_compiler: false, # optional config that provides a map of available compilers # on different systems compilers: %{ # key (`:os.type()`) # this allows us to provide different available targets # on different systems # value is a map that describes which compilers are available # # key == {:unix, :linux} => when compiling on Linux {:unix, :linux} => %{ # key (target triplet) => `riscv64-linux-gnu` # value => `PREFIX` # - for strings, the string will be used as the prefix of # the C and C++ compiler respectively, i.e., # CC=`#{prefix}gcc` # CXX=`#{prefix}g++` "riscv64-linux-gnu" => "riscv64-linux-gnu-", # key (target triplet) => `armv7l-linux-gnueabihf` # value => `{CC, CXX}` # - for 2-tuples, the elements are the executable name of # the C and C++ compiler respectively "armv7l-linux-gnueabihf" => { "arm-linux-gnueabihf-gcc", "arm-linux-gnueabihf-g++" }, # key (target triplet) => `armv7l-linux-gnueabihf` # value => `{CC_EXECUTABLE, CXX_EXECUTABLE, CC_TEMPLATE, CXX_TEMPLATE}` # # - for 4-tuples, the first two elements are the same as in # 2-tuple, the third and fourth elements are the template # string for CC and CPP/CXX. for example, # # the last entry below shows the example of using zig as the # crosscompiler for `aarch64-linux-musl`, # the "CC" will be # "zig cc -target aarch64-linux-musl", # and "CXX" and "CPP" will be # "zig c++ -target aarch64-linux-musl" "aarch64-linux-musl" => { "zig", "zig", "<% cc %> cc -target aarch64-linux-musl", "<% cxx %> c++ -target aarch64-linux-musl" } }, # key == {:unix, :darwin} => when compiling on macOS {:unix, :darwin} => %{ # key (target triplet) => `aarch64-apple-darwin` # value => `{CC, CXX}` "aarch64-apple-darwin" => { "gcc -arch arm64", "g++ -arch arm64" }, # key (target triplet) => `aarch64-linux-musl` # value => `{CC_EXECUTABLE, CXX_EXECUTABLE, CC_TEMPLATE, CXX_TEMPLATE}` "aarch64-linux-musl" => { "zig", "zig", "<% cc %> cc -target aarch64-linux-musl", "<% cxx %> c++ -target aarch64-linux-musl" }, # key (target triplet) => `my-custom-target` # - for 3-tuples, the first element should be `:script` # the second element is the path to the elixir script file # the third element is a 2-tuple, # the first one is the name of the module # the second one is custom args # the module need to impl the `compile/5` callback declared in # `CCPrecompiler.CompilationScript` "my-custom-target" => { :script, "custom.exs", {CustomCompile, []} }, # key (target triplet) => `macos-universal` # on macOS, CCPrecompiler also provides a builtin module to create # universal binary for NIF libraries that only has a `nif.so` file "macos-universal" => { :script, "", {CCPrecompiler.UniversalBinary, []} } } } ] ] ``` `CCPrecompiler.CompilationScript` is defined as follows, ```elixir defmodule CCPrecompiler.CompilationScript do @callback compile( app :: atom(), version :: String.t(), nif_version :: String.t(), target :: String.t(), command_line_args :: [String.t()], custom_args :: [String.t()] ) :: :ok | {:error, String.t()} end ``` ### Custom Compilation Script Examples #### Compile with `ccache` ```elixir defmodule CCPrecompiler.CCache do @moduledoc """ Compile with ccache ## Example "x86_64-linux-gnu" => { :script, "custom.exs", {CCPrecompiler.CCache, []} } It's also possible to do this using a 4-tuple: "x86_64-linux-musl" => { "gcc", "g++", "ccache <% cc %>", "ccache <% cxx %>" } """ @behaviour CCPrecompiler.CompilationScript @impl CCPrecompiler.CompilationScript def compile(app, version, nif_version, target, args, _custom_args) do System.put_env("CC", "ccache gcc") System.put_env("CXX", "ccache g++") System.put_env("CPP", "ccache g++") ElixirMake.Precompiler.mix_compile(args) end end ``` #### Build A Universal NIF Binary on macOS File can be found at `lib/complation_script/universal_binary.ex`. ```elixir defmodule CCPrecompiler.UniversalBinary do @moduledoc """ Build a universal binary on macOS ## Example "macos-universal" => { :script, "universal_binary.exs", {CCPrecompiler.UniversalBinary, []} } """ @behaviour CCPrecompiler.CompilationScript @impl CCPrecompiler.CompilationScript def compile(_app, _version, _nif_version, _target, args, _custom_args) do config = Mix.Project.config() app_priv = Path.join(Mix.Project.app_path(config), "priv") make_precompiler_filename = config[:make_precompiler_filename] || "nif" nif_file = "#{make_precompiler_filename}.so" compiled_bin = Path.join(app_priv, nif_file) x86_64_bin = Path.join(app_priv, "#{make_precompiler_filename}_x86_64.so") aarch64_bin = Path.join(app_priv, "#{make_precompiler_filename}_aarch64.so") File.rm(compiled_bin) # first we compile `x86_64-apple-darwin` :ok = System.put_env("CC", "gcc -arch x86_64") System.put_env("CXX", "gcc -arch x86_64") System.put_env("CPP", "g++ -arch x86_64") ElixirMake.Compiler.compile(args) File.rename!(compiled_bin, x86_64_bin) # then we compile `aarch64-apple-darwin` System.put_env("CC", "gcc -arch arm64") System.put_env("CXX", "gcc -arch arm64") System.put_env("CPP", "g++ -arch arm64") ElixirMake.Compiler.compile(args) File.rename!(compiled_bin, aarch64_bin) {%IO.Stream{}, exit_status} = System.cmd("lipo", ["-create", "-output", compiled_bin, x86_64_bin, aarch64_bin]) File.rm!(x86_64_bin) File.rm!(aarch64_bin) if exit_status == 0 do :ok else Mix.raise("Failed to create universal binary") end end end ```