Packages

A mix compiler for the ASN.1 format leveraging Erlang's `:asn1_ct`.

Current section

Files

Jump to
asn1_compiler lib mix tasks compile.asn1.ex
Raw

lib/mix/tasks/compile.asn1.ex

defmodule Mix.Tasks.Compile.Asn1 do
@moduledoc """
A mix compiler for the ASN.1 format leveraging Erlang's `:asn1_ct`.
Once installed, the compiler can be enabled by changing project configuration in `mix.exs`:
def project() do
[
# ...
compilers: [:asn1] ++ Mix.compilers(),
asn1_options: [:maps]
]
end
Then, you can place your `.asn1` files in the `asn1` folder. The files will be compiled to `src`
as Erlang modules that will be picked up by the Erlang compiler.
The `:asn1_ct` compiler accepts many options that are described in the
[documentation](http://erlang.org/doc/man/asn1ct.html#compile-1) - they can be passed using the
`asn1_options` project configuration (in the same place where the `compilers` configuration
lives). It is recommended to at least set the options to `[:maps]` so that the decoding
and encoding passes use maps rather than records.
## Command line options
* `--force` - forces compilation regardless of modification times
* `--verbose` - inform about each compiled file
## Configuration
* `:asn1_paths` - directories to find source files. Defaults to `["asn1"]`.
* `:erlc_paths` - directories to store generated source files. Defaults to `["src"]` (also
used by the erlang compiler).
* `:asn1_options` - compilation options that apply to ASN.1's compiler.
All available options are descrived in the
[documentation](http://erlang.org/doc/man/asn1ct.html#compile-2).
"""
# Support Elixir <= 1.6
if Code.ensure_loaded?(Mix.Task.Compiler) do
use Mix.Task.Compiler
else
use Mix.Task
end
@recursive true
@manifest ".compile.asn1"
@manifest_vsn 1
@min_mtime {{1970, 1, 1}, {0, 0, 0}}
@switches [force: :boolean, verbose: :boolean, warnings_as_errors: :boolean]
@doc """
Runs this task.
"""
def run(args) do
{opts, _, _} = OptionParser.parse(args, switches: @switches)
project = Mix.Project.config()
source_paths = Keyword.get(project, :asn1_paths, ["asn1"])
dest_path = List.first(Keyword.fetch!(project, :erlc_paths))
verbose? = Keyword.get(opts, :verbose, false)
# TODO: warnings_as_errors
options = Keyword.get(project, :ans1_options, [])
File.mkdir_p!(dest_path)
targets = extract_targets(source_paths, dest_path, Keyword.get(opts, :force, false))
compile(manifest(), targets, verbose?, fn input, output ->
options = options ++ [:noobj, outdir: to_charlist(Path.dirname(output))]
:asn1ct.compile(to_charlist(input), options)
end)
end
@doc """
Returns manifests used by this compiler.
"""
def manifests(), do: [manifest()]
defp manifest(), do: Path.join(Mix.Project.manifest_path(), @manifest)
@doc """
Cleans up compilation artifacts.
"""
def clean() do
remove_files(read_manifest(manifest()))
File.rm(manifest())
end
defp module_files(dir, module) do
[
Path.join(dir, "#{module}.erl"),
Path.join(dir, "#{module}.hrl"),
Path.join(dir, "#{module}.asn1db")
]
end
defp extract_targets(source_paths, dest_path, force?) do
for source <- extract_files(List.wrap(source_paths), ["asn1", "asn", "py"]) do
module = module_name(source)
if force? or stale?(source, module_files(dest_path, module)) do
{:stale, source, Path.join(dest_path, "#{module}.erl")}
else
{:ok, source, Path.join(dest_path, "#{module}.erl")}
end
end
end
defp module_name(file) do
file |> Path.basename() |> Path.rootname()
end
defp extract_files(paths, exts) when is_list(exts) do
extract_files(paths, "*.{#{Enum.join(exts, ",")}}")
end
defp extract_files(paths, pattern) do
Enum.flat_map(paths, fn path ->
case File.stat(path) do
{:ok, %{type: :directory}} -> Path.wildcard("#{path}/**/#{pattern}")
{:ok, %{type: :regular}} -> [path]
_ -> []
end
end)
|> Enum.uniq()
end
defp stale?(source, targets) do
modified_target =
targets
|> Enum.map(&last_modified/1)
|> Enum.reject(&(&1 == @min_mtime))
|> Enum.min(fn -> @min_mtime end)
last_modified(source) > modified_target
end
defp last_modified(file) do
case File.stat(file) do
{:ok, %{mtime: mtime}} -> mtime
{:error, _} -> @min_mtime
end
end
defp compile(manifest, targets, verbose?, callback) do
stale = for {:stale, src, dest} <- targets, do: {src, dest}
previous = read_manifest(manifest)
removed = Enum.reject(previous, fn dest -> Enum.any?(targets, &match?({_, _, ^dest}, &1)) end)
entries =
Enum.reject(previous, fn dest ->
dest in removed || Enum.any?(stale, &match?({_, ^dest}, &1))
end)
if stale == [] and removed == [] do
:noop
else
remove_files(removed)
compiling_n(length(stale), "asn1")
{status, new_entries} = compile(stale, callback, verbose?)
write_manifest(manifest, entries ++ new_entries)
status
end
end
defp compile(stale, callback, verbose?) do
stale
|> Enum.map(fn {input, output} ->
case callback.(input, output) do
:ok ->
verbose? && Mix.shell().info("Compiled #{input}")
{:ok, [output]}
{:error, _errors} ->
{:error, []}
end
end)
|> Enum.reduce({:ok, []}, fn {status1, entries1}, {status2, entries2} ->
status = if status1 == :error or status2 == :error, do: :error, else: :ok
{status, entries1 ++ entries2}
end)
end
defp compiling_n(1, ext), do: Mix.shell().info("Compiling 1 file (.#{ext})")
defp compiling_n(n, ext), do: Mix.shell().info("Compiling #{n} files (.#{ext})")
defp remove_files(to_remove) do
to_remove
|> Enum.flat_map(&module_files(Path.dirname(&1), module_name(&1)))
|> Enum.each(&File.rm/1)
end
defp read_manifest(file) do
try do
file |> File.read!() |> :erlang.binary_to_term()
rescue
_ -> []
else
{@manifest_vsn, data} when is_list(data) -> data
_ -> []
end
end
defp write_manifest(file, entries) do
Path.dirname(file) |> File.mkdir_p!()
File.write!(file, :erlang.term_to_binary({@manifest_vsn, entries}))
end
end