Current section
Files
Jump to
Current section
Files
lib/zig/command.ex
defmodule Zig.Command do
@moduledoc false
# contains all parts of the Zig library involved in calling the
# zig compiler toolchain, especially with regards to the `zig` command, except
# for assembling the build.zig file, which is performed by the
# `Zig.Builder` module.
alias Zig.Builder
alias Zig.Target
require Logger
require Zig
#############################################################################
## API
defp run_zig(command, module) do
args = String.split(command)
base_opts = Keyword.take(module, [:cd, :stderr_to_stdout])
zig_cmd = executable_path()
Logger.debug("running command: #{zig_cmd} #{command}")
case System.cmd(zig_cmd, args, base_opts) do
{result, 0} ->
result
{error, code} ->
raise Zig.CompileError, command: command, code: code, error: error
end
end
require EEx
sema_command = Path.join(__DIR__, "templates/sema_command.eex")
EEx.function_from_file(:defp, :sema_command, sema_command, [:assigns])
def run_sema!(file, opts) do
# TODO: add availability of further options here.
priv_dir = :code.priv_dir(:zigler)
sema_file = Path.join(priv_dir, "beam/sema.zig")
beam_file = Path.join(priv_dir, "beam/beam.zig")
erl_nif_file = Path.join(priv_dir, "beam/stub_erl_nif.zig")
attribs_file = opts[:attribs_file]
c = maybe_add_windows_shim(opts[:c])
# nerves will put in a `CC` command that we need to bypass because it misidentifies
# libc locations for statically linking it.
System.delete_env("CC")
analyte_deps =
[:erl_nif, :beam] ++
List.wrap(if attribs_file, do: [:attributes])
mods =
[erl_nif: %{path: erl_nif_file}, beam: %{deps: [:erl_nif], path: beam_file}] ++
List.wrap(if attribs_file, do: [attributes: %{path: attribs_file}]) ++
[analyte: %{deps: analyte_deps, path: file}]
sema_command(
sema: sema_file,
mods: mods,
c: c
)
|> IO.iodata_to_binary()
|> String.split()
|> Enum.join(" ")
|> run_zig(stderr_to_stdout: true)
end
defp maybe_add_windows_shim(c) do
# TODO: replace this with Target info
case :os.type() do
{_, :nt} ->
:zigler
|> :code.priv_dir()
|> Path.join("erl_nif_win")
|> then(&%{c | include_dirs: [&1 | c.include_dirs]})
_ ->
c
end
end
# documentation requires a separate pathway because otherwise compilation
# doesn't go that well.
# TODO: unify this with the normal sema function using options.
def run_sema_doc!(file) do
priv_dir = :code.priv_dir(:zigler)
sema_file = Path.join(priv_dir, "beam/sema_doc.zig")
erl_nif_file = Path.join(priv_dir, "beam/stub_erl_nif.zig")
analyte_deps = [:erl_nif]
mods = [erl_nif: %{path: erl_nif_file}, analyte: %{deps: analyte_deps, path: file}]
sema_command(
sema: sema_file,
mods: mods
)
|> IO.iodata_to_binary()
|> String.split()
|> Enum.join(" ")
|> run_zig(stderr_to_stdout: true)
end
def fmt(file) do
if System.get_env("ZIG_FMT", "true") != "false" do
run_zig("fmt #{file}", [])
end
end
def compile!(module) do
staging_directory = Builder.staging_directory(module.module)
so_dir = :code.priv_dir(module.otp_app)
lib_dir = Path.join(so_dir, "lib")
run_zig("build -Doptimize=#{release_mode(module)} --prefix #{so_dir}",
cd: staging_directory,
stderr_to_stdout: true
)
case :os.type() do
{_, :nt} ->
# windows dlls wind up in the bin directory instead of the lib directory.
bin_dir = Path.join(so_dir, "bin")
src_lib_path = Path.join(bin_dir, src_lib_name(module.module))
dst_lib_path = Path.join(lib_dir, dst_lib_name(module.module))
# on Windows, not deleting this causes a file permissions error.
File.rm(dst_lib_path)
File.cp!(src_lib_path, dst_lib_path)
Logger.debug("built library at #{dst_lib_path}")
_ ->
src_lib_path = Path.join(lib_dir, src_lib_name(module.module))
dst_lib_path = Path.join(lib_dir, dst_lib_name(module.module))
# on MacOS, we must delete the old library because otherwise library
# integrity checker will kill the process
File.rm(dst_lib_path)
File.cp!(src_lib_path, dst_lib_path)
Logger.debug("built library at #{dst_lib_path}")
end
module
rescue
e in Zig.CompileError ->
reraise Zig.CompileError.resolve(e, module), __STACKTRACE__
end
def targets do
run_zig("targets", [])
end
@release_modes %{
debug: "Debug",
fast: "ReleaseFast",
small: "ReleaseSmall",
safe: "ReleaseSafe"
}
def release_mode(%{release_mode: :env}) do
System.fetch_env!("ZIGLER_RELEASE_MODE")
end
def release_mode(%{release_mode: {:env, mode}}) do
case System.get_env("ZIGLER_RELEASE_MODE") do
env when env in ["", nil] ->
Map.fetch!(@release_modes, mode)
supplied ->
supplied
end
end
def release_mode(module) do
Map.fetch!(@release_modes, module.release_mode)
end
def executable_path do
# executable_path resolves zig executable in the following fashion:
#
# 1. check for zig in `ZIG_ARCHIVE_PATH` env path
# 2. check for zig cached in `:filename.basedir/3`
# 3. look for zig using System.find_executable
#
cond do
path = find_from_env() ->
Logger.info("zig expected (via env variable) in #{path}")
path
path = find_in_basedir() ->
Logger.info("zig expected (via cache) in #{path}")
path
path = System.find_executable(zig_cmd_name()) ->
Logger.info("system zig found in #{path}")
path
true ->
raise CompileError, description: "zig executable not found"
end
end
defp find_from_env do
cond do
(path = System.get_env("ZIG_ARCHIVE_PATH", "")) != "" ->
versioned_path(path)
(path = System.get_env("ZIG_EXECUTABLE_PATH", "")) != "" ->
if File.exists?(path), do: path
:else ->
nil
end
end
@default_version Zig.version()
defp find_in_basedir do
:user_cache
|> :filename.basedir("zigler")
|> versioned_path()
end
defp versioned_path(path) do
{os, arch} = Zig.Get.os_info()
zig_executable = Path.join(path, "zig-#{arch}-#{os}-#{@default_version}/#{zig_cmd_name()}")
Logger.info("searching for zig in #{zig_executable}")
if File.exists?(zig_executable), do: zig_executable
end
defp src_lib_name(module) do
case {Target.resolve(), :os.type()} do
{nil, {:unix, :darwin}} ->
"lib#{module}.dylib"
{nil, {_, :nt}} ->
"#{module}.dll"
_ ->
"lib#{module}.so"
end
end
defp dst_lib_name(module) do
case {Target.resolve(), :os.type()} do
{nil, {:unix, :darwin}} ->
"#{module}.so"
{nil, {_, :nt}} ->
"#{module}.dll"
_ ->
"#{module}.so"
end
end
defp zig_cmd_name do
case :os.type() do
{_, :nt} -> "zig.exe"
_ -> "zig"
end
end
# utility function to split on CR(/LF). This could be platform-dependent.
def split_on_newline(str) do
str
|> String.split("\n")
|> Enum.map(&String.trim_trailing(&1, "\r"))
end
def newline do
case :os.type() do
{_, :nt} -> "\r\n"
_ -> "\n"
end
end
end