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.{Builder, Patches} require Logger ############################################################################# ## API def compile(compiler, zig_tree) do zig_executable = if compiler.module_spec.local_zig do System.find_executable("zig") else # apply patches, if applicable Patches.sync(zig_tree) Path.join(zig_tree, "zig") end opts = [cd: compiler.assembly_dir, stderr_to_stdout: true] Logger.debug("compiling nif for module #{inspect compiler.module_spec.module} in path #{compiler.assembly_dir}") Builder.build(compiler, zig_tree) case System.cmd(zig_executable, ["build"], opts) do {_, 0} -> :ok {err, _} -> alias Zig.Parser.Error Error.parse(err, compiler) end lib_dir = compiler.module_spec.otp_app |> :code.lib_dir() |> Path.join("ebin") library_filename = Zig.nif_name(compiler.module_spec) # copy the compiled library over to the lib/nif directory. File.mkdir_p!(lib_dir) compiler.assembly_dir |> Path.join("zig-cache/lib/#{library_filename}") |> File.cp!(Path.join(lib_dir, library_filename)) # link the compiled library to be unversioned. symlink_filename = Path.join(lib_dir, "#{library_filename}") unless File.exists?(symlink_filename) do lib_dir |> Path.join(library_filename) |> File.ln_s!(symlink_filename) end :ok end ############################################################################# ## download zig from online sources. @doc false def version_name(version) do "zig-#{get_os()}-#{get_arch()}-#{version}" end def get_os do case :os.type do {:unix, :linux} -> "linux" {:unix, :freebsd} -> "freebsd" {:unix, :darwin} -> Logger.warn("macos support is experimental") "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 Logger.warn(""" windows is not supported, but may work. If you find an error in the process, please leave an issue at: https://github.com/ityonemo/zigler/issues """) end @zig_dir_path Path.expand("../../zig", Path.dirname(__ENV__.file)) def fetch(version) do zig_dir = Path.join(@zig_dir_path, version_name(version)) 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 = version_name(version) <> extension 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 # https://ziglang.org/download/#release-0.7.1 @checksums %{ "zig-freebsd-x86_64-0.7.1.tar.xz" => "e73c1dca35791a3183fdd5ecde0443ebbe180942efceafe651886034fb8def09", "zig-linux-aarch64-0.7.1.tar.xz" => "48ec90eba407e4587ddef7eecef25fec7e13587eb98e3b83c5f2f5fff2a5cbe7", "zig-linux-armv7a-0.7.1.tar.xz" => "5a0662e07b4c4968665e1f97558f8591f6facec45d2e0ff5715e661743107ceb", "zig-linux-i386-0.7.1.tar.xz" => "4882e052e5f83690bd0334bb4fc1702b5403cb3a3d2aa63fd7d6043d8afecba3", "zig-linux-riscv64-0.7.1.tar.xz" => "187294bfd35983348c3fe042901b42e67e7e36ab7f77a5f969d21c0051f4d21f", "zig-linux-x86_64-0.7.1.tar.xz" => "18c7b9b200600f8bcde1cd8d7f1f578cbc3676241ce36d771937ce19a8159b8d", "zig-macos-x86_64-0.7.1.tar.xz" => "845cb17562978af0cf67e3993f4e33330525eaf01ead9386df9105111e3bc519", "zig-windows-i386-0.7.1.zip" => "a1b9a7421e13153e07fd2e2c93ff29aad64d83105b8fcdafa633dbe689caf1c0", "zig-windows-x86_64-0.7.1.zip" => "4818a8a65b4672bc52c0ae7f14d014e0eb8caf10f12c0745176820384cea296a" } 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