defmodule DtsBuddy.Backend do @moduledoc """ Collection of functions that actually check system status, compile DTS files, check overlay status. """ alias DtsBuddy.Paths @doc """ Calls `mount -t configfs none /sys/kernel/config`. """ @spec enable_overlays() :: :ok | {:error, {any(), pos_integer()}} def enable_overlays do case System.cmd("mount", ["-t", "configfs", "none", Paths.config_dir()]) do {_, 0} -> :ok e -> {:error, e} end end @doc """ Checks the presence of the `dtc` binary. """ @spec device_tree_compiler_present?() :: boolean() def device_tree_compiler_present?, do: !is_nil(System.find_executable("dtc")) @doc """ Checks the presence of the /sys/kernel/config/device-tree/overlays directory. """ @spec overlays_enabled?() :: boolean() def overlays_enabled?, do: File.exists?(Paths.overlays_dir()) @doc """ Helper to display all system checks. Quite frugal for now, but should be extended. """ @spec checks() :: [ {:device_tree_compiler_present, boolean()} | {:overlays_enabled, boolean()} ] def checks do [ overlays_enabled: overlays_enabled?(), device_tree_compiler_present: device_tree_compiler_present?() ] end @doc """ Loads an overlay. This function is meant to be directly called with the output of DtsBuddy.compile/2 or DtsBuddy.compile_eex/3, that is, a tuple of the form `{:error, term()}` or `{:ok, path, name}` where path is the location of the compiled dtbo file, and name is the desired overlay name. """ @spec load({:error, any} | {:ok, binary(), binary()}) :: {:error, any()} | :ok def load({:ok, path, name}), do: do_load(path, name) def load({:error, e}), do: {:error, e} @spec do_load(binary(), binary()) :: :ok | {:error, any()} defp do_load(path, name) do with :ok <- validate_name(name), :ok <- File.mkdir_p(Paths.overlay_dir(name)), {:ok, contents} <- File.read(path) do File.write(Paths.dtbo_dir(name), contents) end end @spec validate_name(binary()) :: :ok | {:error, {:invalid_name, any()}} defp validate_name(name) do case Paths.valid_name?(name) do true -> :ok false -> {:error, {:invalid_name, name}} end end @doc """ Reads /sys/kernel/config/device-tree/overlays//status and gives the result as `:applied` or `:unapplied`. """ @spec status(binary()) :: :applied | :unapplied def status(name) do case File.read(Paths.status_dir(name)) do {:ok, "applied" <> _} -> :applied _ -> :unapplied end end @doc """ Injects an EEx template and compiles it. """ @spec compile_eex(binary(), keyword(), binary()) :: {:error, {any(), pos_integer()}} | {:ok, binary(), binary()} def compile_eex(str, bindings, name) do dts_string = EEx.eval_string(str, bindings) compile(dts_string, "#{name}") end @spec do_compile(binary(), binary()) :: :ok | {:error, any()} defp do_compile(dts, dtbo) do case System.cmd("dtc", [ "-@", "-I", "dts", "-O", "dtb", "-o", dtbo, dts ]) do {_, 0} -> :ok e -> {:error, e} end end @doc """ Compiles a static DTS string to the given name. Files written are /data/.dts and /data/.dtbo. The name is used verbatim to build filesystem paths and is validated with `DtsBuddy.Paths.valid_name?/1`; an invalid name returns `{:error, {:invalid_name, name}}` without touching the filesystem. """ @spec compile(binary(), binary()) :: {:error, any()} | {:ok, binary(), binary()} def compile(dts_string, name) do with :ok <- validate_name(name), dts_file = Paths.dts_file_path(name), dtbo_file = Paths.dtbo_file_path(name), {_, :ok} <- {:write, File.write(dts_file, dts_string)}, {_, :ok} <- {:compile, do_compile(dts_file, dtbo_file)} do {:ok, dtbo_file, name} else {:error, {:invalid_name, _}} = e -> e {:write, _} -> {:error, "Writing the DTS file failed. Please verify the name provided."} {:compile, e} -> {:error, {"Compiling the DTS file failed. Here is output from DTC :", e}} end end end