defmodule Zig.Command do
@moduledoc """
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.Assembler
alias Zig.Target
require Logger
#############################################################################
## API
defp run_zig(command, opts) do
args = String.split(command)
base_opts = Keyword.take(opts, [:cd, :stderr_to_stdout])
zig_cmd = executable_path(opts)
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
def run_sema(file, opts) do
priv_dir = :code.priv_dir(:zigler)
sema_file = Keyword.get(opts, :sema_context, 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")
package_opts =
opts
|> Keyword.get(:packages)
|> List.wrap()
erl_nif_pkg = {:erl_nif, erl_nif_file}
package_files =
Enum.map(package_opts, fn {name, {path, _}} -> {name, path} end) ++
[beam: {beam_file, [erl_nif_pkg]}, erl_nif: erl_nif_file]
packages =
Enum.map(package_opts, fn
{name, path} when is_binary(path) ->
{name, path}
{name, {path, []}} ->
{name, path}
{name, {path, deps}} ->
deps_keyword = Enum.map(deps, &{&1, Keyword.fetch!(package_files, &1)})
{name, {path, deps_keyword}}
end)
beam_pkg = {:beam, {beam_file, [erl_nif_pkg]}}
packages =
[
erl_nif_pkg,
beam_pkg,
analyte:
{file,
[
beam_pkg,
erl_nif_pkg
] ++ packages}
]
deps =
packages
|> package_deps()
|> String.replace_prefix("", "--deps ")
mods =
packages
|> package_mods()
|> Enum.join(" ")
# 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")
sema_command = "run #{sema_file} #{deps} #{mods} -lc #{link_opts(opts)}"
# Need to make this an OK tuple
{:ok, run_zig(sema_command, stderr_to_stdout: true)}
end
defp package_deps(packages) do
packages
|> Keyword.keys()
|> Enum.map_join(",", &to_string/1)
end
defp package_mods(packages) do
packages
|> Enum.flat_map(fn
{name, {file, deps}} ->
["--mod #{name}:#{package_deps(deps)}:#{file}"] ++ package_mods(deps)
{name, file} ->
["--mod #{name}::#{file}"]
end)
|> Enum.uniq()
end
defp link_opts(opts) do
opts
|> Keyword.fetch!(:include_dir)
|> Enum.map_join(" ", &"-I #{&1}")
end
def fmt(file) do
run_zig("fmt #{file}", [])
end
def compile(module, opts) do
assembly_dir = Assembler.directory(module)
so_dir =
opts
|> Keyword.fetch!(:otp_app)
|> :code.priv_dir()
lib_dir = Path.join(so_dir, "lib")
run_zig("build --prefix #{so_dir}", cd: assembly_dir)
src_lib_name = Path.join(lib_dir, src_lib_name(module))
dst_lib_name = Path.join(lib_dir, dst_lib_name(module))
# on MacOS, we must delete the old library because otherwise library
# integrity checker will kill the process
File.rm(dst_lib_name)
File.cp!(src_lib_name, dst_lib_name)
Logger.debug("built library at #{dst_lib_name}")
end
def targets do
run_zig("targets", [])
end
defp executable_path(opts) do
cond do
opts[:local_zig] -> System.find_executable("zig")
path = opts[:zig_path] -> path
true -> Path.join(directory(), "zig")
end
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
#############################################################################
## download zig from online sources.
# TODO: move to target using :host as a parameter.
@doc false
def os_arch do
"#{get_os()}-#{get_arch()}"
end
def get_os do
case :os.type() do
{:unix, :linux} ->
"linux"
{:unix, :freebsd} ->
"freebsd"
{:unix, :darwin} ->
"macos"
{_, :nt} ->
windows_warn()
"windows"
end
end
@arches %{
"i386" => "i386",
"i486" => "i386",
"i586" => "i386",
"x86_64" => "x86_64",
"armv6" => "armv6kz",
"armv7" => "armv7a",
"aarch64" => "aarch64",
"amd64" => "x86_64",
"win32" => "i386",
"win64" => "x86_64"
}
# note this is the architecture of the machine where compilation is
# being done, not the target architecture of cross-compiled
def get_arch do
arch =
:system_architecture
|> :erlang.system_info()
|> List.to_string()
Enum.find_value(@arches, fn
{prefix, zig_arch} -> if String.starts_with?(arch, prefix), do: zig_arch
end) || raise arch_warn()
end
defp arch_warn,
do: """
it seems like you are compiling from an unsupported architecture:
#{:erlang.system_info(:system_architecture)}
Please leave an issue at https://github.com/ityonemo/zigler/issues
"""
defp windows_warn do
raise "windows is not supported, and will be supported in zigler 0.11"
end
@zig_dir_path Path.expand("../../zig", Path.dirname(__ENV__.file))
defp directory, do: Path.join(@zig_dir_path, "zig-#{os_arch()}-#{Zig.version()}")
# TODO: rename this.
def fetch!(version) do
zig_dir = directory()
zig_executable = Path.join(zig_dir, "zig")
:global.set_lock({__MODULE__, self()})
unless File.exists?(zig_executable) do
# make sure the zig directory path exists and is ready.
File.mkdir_p!(@zig_dir_path)
# make sure that we're in the correct operating system.
extension =
if match?({_, :nt}, :os.type()) do
".zip"
else
".tar.xz"
end
archive = "zig-#{os_arch()}-#{Zig.version()}#{extension}"
# TODO: clean this up.
Logger.configure(level: :info)
zig_download_path = Path.join(@zig_dir_path, archive)
download_zig_archive(zig_download_path, version, archive)
# untar the zig directory.
unarchive_zig(archive)
end
:global.del_lock({__MODULE__, self()})
end
defp download_zig_archive(zig_download_path, version, archive) do
url = "https://ziglang.org/download/#{version}/#{archive}"
Logger.info("downloading zig version #{version} (#{url}) and caching in #{@zig_dir_path}.")
case httpc_get(url) do
{:ok, %{status: 200, body: body}} ->
# expected_checksum = Map.fetch!(@checksums, archive)
# actual_checksum = :sha256 |> :crypto.hash(body) |> Base.encode16(case: :lower)
# if expected_checksum != actual_checksum do
# raise "checksum mismatch: expected #{expected_checksum}, got #{actual_checksum}"
# end
File.write!(zig_download_path, body)
_ ->
raise "failed to download the appropriate zig archive."
end
end
defp httpc_get(url) do
{:ok, _} = Application.ensure_all_started(:ssl)
{:ok, _} = Application.ensure_all_started(:inets)
headers = []
request = {String.to_charlist(url), headers}
http_options = [timeout: 600_000]
options = [body_format: :binary]
case :httpc.request(:get, request, http_options, options) do
{:ok, {{_, status, _}, headers, body}} ->
{:ok, %{status: status, headers: headers, body: body}}
other ->
other
end
end
def unarchive_zig(archive) do
System.cmd("tar", ["xvf", archive], cd: @zig_dir_path)
end
end