Packages

An Elixir framework to build your own in-house PaaS (Platform as a Service).

Current section

Files

Jump to
antikythera mix_common.exs
Raw

mix_common.exs

# Copyright(c) 2015-2018 ACCESS CO., LTD. All rights reserved.
#
# Modules defined in this file provides common project settings
# for antikythera itself, antikythera instances and also gears.
# These modules must be loadable independently of `Antikythera.Mixfile`, i.e.,
# they must be defined in a file separate from `mix.exs`
# (if we put these modules in `mix.exs`, mix complains about redefinition of the same mix project).
#
defmodule Antikythera.MixCommon do
versions =
File.read!(Path.join(__DIR__, ".tool-versions"))
|> String.split("\n", trim: true)
|> Map.new(fn line -> [n, v] = String.split(line, " ", trim: true); {n, v} end)
@otp_version Map.fetch!(versions, "erlang")
@elixir_version Map.fetch!(versions, "elixir")
# Strictly enforce Erlang/OTP version
otp_version_path = Path.join([:code.root_dir(), "releases", :erlang.system_info(:otp_release), "OTP_VERSION"])
current_otp_version = File.read!(otp_version_path) |> String.trim_trailing()
if current_otp_version != @otp_version do
Mix.raise("Incorrect Erlang/OTP version! required: '#{@otp_version}', used: '#{current_otp_version}'")
end
def common_project_settings() do
[
elixir: @elixir_version, # Enforce elixir version
elixirc_options: [warnings_as_errors: true],
build_embedded: Mix.env() == :prod,
test_coverage: [tool: ExCoveralls],
preferred_cli_env: ["coveralls": :test, "coveralls.detail": :test, "coveralls.html": :test, "antikythera_local.upgrade_compatibility_test": :test],
]
end
#
# deps
#
def filter_tool_deps(deps) do
Enum.reject(deps, &dep_available_in_env?(&1, :prod))
end
defp dep_available_in_env?(dep, env) do
extract_dep_options(dep) |> Keyword.get(:only, [:dev, :test, :prod]) |> List.wrap() |> Enum.member?(env)
end
defp dep_used_at_runtime?(dep) do
extract_dep_options(dep) |> Keyword.get(:runtime, true)
end
defp dep_indirect?(dep) do
extract_dep_options(dep) |> Keyword.get(:indirect, false)
end
defp dep_antikythera_internal?(dep) do
extract_dep_options(dep) |> Keyword.get(:antikythera_internal, false)
end
defp extract_dep_options({_name, _ver, opts}) , do: opts
defp extract_dep_options({_name, opts}) when is_list(opts), do: opts
defp extract_dep_options(_) , do: []
#
# runtime dependencies (`applications` field in .app file)
#
def antikythera_runtime_dependency_applications(deps) do
runtime_applications_from_deps =
deps
|> Enum.filter(&dep_available_in_env?(&1, Mix.env()))
|> Enum.filter(&dep_used_at_runtime?/1)
|> Enum.reject(&dep_indirect?/1)
|> Enum.map(&elem(&1, 0))
runtime_applications_from_deps ++ special_antikythera_internal_applications() ++ applications_required_in_dev_for_dialyzer()
end
def gear_runtime_dependency_applications(deps) do
runtime_applications_from_deps =
deps
|> Enum.filter(&dep_available_in_env?(&1, Mix.env()))
|> Enum.filter(&dep_used_at_runtime?/1)
|> Enum.reject(&dep_indirect?/1)
|> Enum.reject(&dep_antikythera_internal?/1)
|> Enum.map(&elem(&1, 0))
runtime_applications_from_deps ++ applications_required_in_dev_for_dialyzer()
end
defp special_antikythera_internal_applications() do
[
:sasl, # To use :release_handler
:inets, # To use :httpd_util
:crypto,
:mnesia,
:logger,
:p1_utils, # :iconv does not declare this as a runtime dependency; we have to explicitly add this
]
end
defp applications_required_in_dev_for_dialyzer() do
case Mix.env() do
:dev -> [
:mix, # Suppress warning about Mix.Task behaviour
:eex, # Only used during compilation, suppress warning about EEx.Engine behaviour
:ex_unit, # Suppress warnings about calling ExUnit functions in Antikythera.Test.*
]
_ -> []
end
end
#
# util
#
def version_with_last_commit_info(major_minor_patch) do
{git_log_output, 0} = System.cmd("git", ["log", "-1", "--format=%cd", "--date=raw"])
[seconds_str, _timezone] = String.split(git_log_output)
seconds_since_epoch = String.to_integer(seconds_str)
time_as_tuple = {div(seconds_since_epoch, 1_000_000), rem(seconds_since_epoch, 1_000_000), 0}
{{y, mo, d}, {h, mi, s}} = :calendar.now_to_universal_time(time_as_tuple)
last_commit_time = :io_lib.format('~4..0w~2..0w~2..0w~2..0w~2..0w~2..0w', [y, mo, d, h, mi, s]) |> List.to_string()
{last_commit_sha1, 0} = System.cmd("git", ["rev-parse", "HEAD"])
major_minor_patch <> "-" <> last_commit_time <> "+" <> String.trim(last_commit_sha1)
end
end
defmodule Antikythera.MixConfig do
@moduledoc """
Helper module to simplify `config.exs` files in antikythera instance projects.
Antikythera uses a number of mix configurations of many OTP applications for various purposes;
setting the same set of mix configurations in `config.exs` files of all antikythera instance projects
introduces too much boilerplate.
To avoid the boilerplate, you can call `Antikythera.MixConfig.all/0` to obtain the default values
and then set the values by `Mix.Config.config/2` macro in your `config.exs` file.
The default values returned by `Antikythera.MixConfig.all/0` should suffice for most cases.
None of the mix configurations of `:antikythera` application are included in the return value
(as there's no appropriate defaults for them); you must properly set them yourself.
For explanations of the configuration items of `:antikythera`, see antikythera's `config.exs` file.
As a result, your `config.exs` file should look like the following:
use Mix.Config
try do
for {app, kw} <- Antikythera.MixConfig.all() do
config(app, kw)
end
rescue
UndefinedFunctionError -> :ok
end
config :antikythera, [
<key1>: <value1>,
...
]
Note that the `try`-`rescue` is needed to correctly bootstrap your project
from the situation where `:antikythera` as a dependency is not yet fetched.
"""
@spec all() :: Keyword.t(Keyword.t(any))
def all() do
default_configs() ++ croma_configs() ++ exsync_configs()
end
defp default_configs() do
[
# Limit port range to be used for ErlangVM-to-ErlangVM communications.
kernel: [
inet_dist_listen_min: 6000,
inet_dist_listen_max: 7999,
],
# Logger configurations.
sasl: [
sasl_error_logger: false, # SASL logs are handled by :logger
],
logger: [
level: :info,
utc_log: true,
handle_sasl_reports: true,
translators: [{AntikytheraCore.ErlangLogTranslator, :translate}],
backends: [:console, AntikytheraCore.Alert.LoggerBackend],
console: [
format: "$dateT$time+00:00 [$level$levelpad] $metadata$message\n",
metadata: [:module],
],
],
# Persist Raft logs & snapshots for async job queues.
raft_fleet: [
per_member_options_maker: AntikytheraCore.AsyncJob.RaftOptionsMaker,
],
]
end
defp croma_configs() do
# (compile-time configuration) Disable croma's runtime validations
# when running as a "deployment" (see also antikythera's `:deployments` option below).
local? = System.get_env("ANTIKYTHERA_COMPILE_ENV") in [nil, "local"]
[
croma: [
defun_generate_validation: local?,
debug_assert: local?,
],
]
end
defp exsync_configs() do
# During gear development, recompile and reload on modifications of HAML files.
if Mix.env() == :dev do
[
exsync: [
extra_extensions: [".haml"],
],
]
else
[]
end
end
end
defmodule Antikythera.GearProject do
@moduledoc """
Module to be `use`d by `Mixfile` module in each gear project.
`__using__/1` of this module receives the following key in its argument.
- (required) `:antikythera_instance_dep` : Dependency on the antikythera instance which this gear belongs to.
- (optional) `:source_url` : If given it's used as both `source_url` (and also `homepage_url`).
The following private functions are used by this module and thus mandatory.
- `gear_name/0` : Name of the gear as an atom.
- `version/0 : Current version of the gear.
- `gear_deps/0 : Dependencies on other gears.
"""
def load_antikythera_instance_mix_config_file!(instance_name) do
# Load mix config to import compile-time configurations;
# if antikythera instance is not yet available, raise `Mix.Config.LoadError` and fallback to `AntikytheraGearInitialSetup`.
config_path = Path.join([antikythera_instance_dir(instance_name), "config", "config.exs"])
Mix.Config.persist(Mix.Config.read!(config_path))
end
def get_antikythera_instance_project_settings!(instance_name) do
Mix.Project.in_project(instance_name, antikythera_instance_dir(instance_name), fn mod ->
mod.project()
end)
end
defp antikythera_instance_dir(instance_name) do
# When `use`d by a gear, this file resides in "deps/antikythera/"; antikythera instance is located at the sibling dir.
Path.join([__DIR__, "..", "#{instance_name}"]) |> Path.expand()
end
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
@antikythera_instance_dep Keyword.fetch!(opts, :antikythera_instance_dep)
@antikythera_instance_name elem(@antikythera_instance_dep, 0)
@antikythera_instance_project Antikythera.GearProject.get_antikythera_instance_project_settings!(@antikythera_instance_name)
@antikythera_instance_deps @antikythera_instance_project[:deps]
@source_url Keyword.get(opts, :source_url)
Antikythera.GearProject.load_antikythera_instance_mix_config_file!(@antikythera_instance_name)
# Deliberately undocumented option; only used by special gears (mostly for testing or administrative purposes)
@use_antikythera_internal_modules? Keyword.get(opts, :use_antikythera_internal_modules?, false)
use Mix.Project
def project() do
[
app: gear_name(),
version: Antikythera.MixCommon.version_with_last_commit_info(version()),
elixirc_paths: ["lib", "web"],
compilers: [:ensure_gear_dependencies, :gettext, :propagate_file_modifications] ++ Mix.compilers() ++ [:gear_static_analysis],
start_permanent: false,
deps: deps(),
docs: [output: "exdoc"],
antikythera_gear: [
instance_dep: @antikythera_instance_dep,
gear_deps: gear_deps(),
use_antikythera_internal_modules?: @use_antikythera_internal_modules?,
],
] ++ urls() ++ Antikythera.MixCommon.common_project_settings()
end
defp urls() do
case @source_url do
nil -> []
url -> [source_url: url, homepage_url: url]
end
end
def application() do
gear_application_module_name = gear_name() |> Atom.to_string() |> Macro.camelize()
gear_application_module = Module.concat([gear_application_module_name])
runtime_dependency_applications = Antikythera.MixCommon.gear_runtime_dependency_applications(@antikythera_instance_deps)
gear_dependency_names = Enum.map(gear_deps(), &elem(&1, 0))
[
mod: {gear_application_module, []},
applications: [@antikythera_instance_name] ++ runtime_dependency_applications ++ gear_dependency_names,
]
end
defp deps() do
# :dev/:test-only deps of antikythera instance are not automatically added to gear projects.
# We must declare the same set of tool deps in gear projects in order
# not only to make tool deps available but also to use the exact version specified by the antikythera instance.
tool_deps = Antikythera.MixCommon.filter_tool_deps(@antikythera_instance_deps)
[@antikythera_instance_dep] ++ tool_deps ++ gear_deps()
end
end
end
end