diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 219c6d1a..de1ad022 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -136,6 +136,55 @@ jobs: make compile.protocols.poncho make dialyzer.all + release-test: + runs-on: ${{matrix.os.name}} + name: Release test (${{matrix.os.name}}) + strategy: + matrix: + os: + - name: ubuntu-latest + target: linux_amd64 + - name: macos-14 + target: darwin_arm64 + - name: macos-13 + target: darwin_amd64 + + include: + - elixir: "1.15.8" + otp: "27" + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} + + - uses: mlugg/setup-zig@v2 + with: + version: "0.14.0" + + - name: Cache deps + id: cache-deps + uses: actions/cache@v4 + with: + path: | + apps/**/deps + apps/**/_build + + key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('apps/**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}- + + - name: Build and release + env: + MIX_ENV: prod + BURRITO_TARGET: ${{ matrix.os.target }} + run: make build.engine && cd apps/expert && mix deps.get && mix release expert + test: runs-on: ubuntu-latest name: Test on OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} @@ -195,29 +244,3 @@ jobs: # Step: Execute the tests. - name: Run tests run: make test.all - - integration_test: - runs-on: ubuntu-latest - name: Integration tests - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build integration runner - uses: docker/build-push-action@v6 - with: - context: . - file: ./integration/Dockerfile - tags: xp - # GitHub Actions cache - # https://docs.docker.com/build/ci/github-actions/cache/ - cache-from: type=gha - cache-to: type=gha,mode=max - # Required to make the image available through docker - load: true - - - name: Run integration tests - run: NO_BUILD=1 ./integration/test.sh diff --git a/.gitignore b/.gitignore index ebde3b90..c4b805b8 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,7 @@ apps/forge/src/future_elixir_parser.erl .DS_Store +# Ignore release artifacts +**/burrito_out + .notes/ diff --git a/Makefile b/Makefile index 89c980da..87d2f41e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,34 @@ poncho_dirs = forge expert_credo engine expert +local_target := +ifeq ($(OS),Windows_NT) + local_target := $(local_target)windows + ifeq ($(PROCESSOR_ARCHITECTURE),AMD64) + local_target := $(local_target)_amd64 + endif + ifeq ($(PROCESSOR_ARCHITECTURE),x86) + local_target := $(local_target)_amd64 + endif +else + UNAME_S := $(shell uname -s) + ifeq ($(UNAME_S),Linux) + local_target := $(local_target)linux + endif + ifeq ($(UNAME_S),Darwin) + local_target := $(local_target)darwin + endif + UNAME_P := $(shell uname -p) + ifeq ($(UNAME_P),x86_64) + local_target := $(local_target)_amd64 + endif + ifneq ($(filter %86,$(UNAME_P)),) + local_target := $(local_target)_amd64 + endif + ifneq ($(filter arm%,$(UNAME_P)),) + local_target := $(local_target)_arm64 + endif +endif + compile.all: compile.poncho dialyzer.all: compile.poncho dialyzer.poncho @@ -41,5 +70,20 @@ dialyzer.plt.poncho: dialyzer.poncho: compile.poncho compile.protocols.poncho $(foreach dir, $(poncho_dirs), cd apps/$(dir) && mix dialyzer && cd ../..;) -package: - cd apps/expert && mix package +build.engine: + cd apps/engine && mix deps.get && MIX_ENV=dev mix build + +release: build.engine + cd apps/expert &&\ + mix deps.get &&\ + EXPERT_RELEASE_MODE=burrito MIX_ENV=prod mix release expert --overwrite + +release.local: build.engine + cd apps/expert &&\ + mix deps.get &&\ + EXPERT_RELEASE_MODE=burrito BURRITO_TARGET=$(local_target) MIX_ENV=prod mix release expert --overwrite + +release.plain: build.engine + cd apps/expert &&\ + mix deps.get &&\ + EXPERT_RELEASE_MODE=plain MIX_ENV=prod mix release plain --overwrite diff --git a/apps/engine/lib/engine/engine/build/project.ex b/apps/engine/lib/engine/engine/build/project.ex index 42df7d47..3480b9f9 100644 --- a/apps/engine/lib/engine/engine/build/project.ex +++ b/apps/engine/lib/engine/engine/build/project.ex @@ -73,11 +73,11 @@ defmodule Engine.Build.Project do defp prepare_for_project_build(true = _initial?) do if connected_to_internet?() do with_progress "mix local.hex", fn -> - Mix.Task.run("local.hex", ~w(--force --if-missing)) + Mix.Task.run("local.hex", ~w(--force)) end with_progress "mix local.rebar", fn -> - Mix.Task.run("local.rebar", ~w(--force --if-missing)) + Mix.Task.run("local.rebar", ~w(--force)) end with_progress "mix deps.get", fn -> diff --git a/apps/engine/priv/.gitkeep b/apps/engine/priv/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/expert/bin/activate_version_manager.sh b/apps/expert/bin/activate_version_manager.sh deleted file mode 100755 index 754ae450..00000000 --- a/apps/expert/bin/activate_version_manager.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env bash - -# The purpose of these functions is to detect and activate the correct -# installed version manager in the current shell session. Currently, we -# try to detect asdf, rtx, and mise (new name for rtx). -# -# The general approach involves the following steps: -# -# 1. Try to detect an already activated version manager that provides -# Elixir. If one is present, there's nothing more to do. -# 2. Try to find and activate an asdf installation. If it provides -# Elixir, we're all set. -# 3. Try to find and activate an rtx installation. If it provides -# Elixir, we're all set. -# 4. Try to find and activate a mise installation. If it provides -# Elixir, we're all set. -# - -activate_version_manager() { - if (_detect_asdf || _detect_rtx || _detect_mise); then - return 0 - fi - - echo >&2 "No activated version manager detected. Searching for version manager..." - - { _try_activating_asdf && _detect_asdf; } || - { _try_activating_rtx && _detect_rtx; } || - { _try_activating_mise && _detect_mise; } - return $? -} - -_detect_asdf() { - if command -v asdf >/dev/null && asdf which elixir >/dev/null 2>&1 && _ensure_which_elixir asdf; then - echo >&2 "Detected Elixir through asdf: $(asdf which elixir)" - return 0 - else - return 1 - fi -} - -_detect_rtx() { - if command -v rtx >/dev/null && rtx which elixir >/dev/null 2>&1 && _ensure_which_elixir rtx; then - echo >&2 "Detected Elixir through rtx: $(rtx which elixir)" - return 0 - else - return 1 - fi -} - -_detect_mise() { - if command -v mise >/dev/null && mise which elixir >/dev/null 2>&1 && _ensure_which_elixir mise; then - echo >&2 "Detected Elixir through mise: $(mise which elixir)" - return 0 - else - return 1 - fi -} - -_ensure_which_elixir() { - [[ $(which elixir) == *"$1"* ]] - return $? -} - -_try_activating_asdf() { - local asdf_dir="${ASDF_DIR:-"$HOME/.asdf"}" - local asdf_vm="$asdf_dir/asdf.sh" - - if test -f "$asdf_vm"; then - echo >&2 "Found asdf. Activating..." - # shellcheck disable=SC1090 - . "$asdf_vm" - return $? - else - return 1 - fi -} - -_try_activating_rtx() { - if which rtx >/dev/null; then - echo >&2 "Found rtx. Activating..." - eval "$(rtx activate bash)" - eval "$(rtx env)" - return $? - else - return 1 - fi -} - -_try_activating_mise() { - if which mise >/dev/null; then - echo >&2 "Found mise. Activating..." - eval "$(mise activate bash)" - eval "$(mise env)" - return $? - else - return 1 - fi -} - -activate_version_manager diff --git a/apps/expert/bin/boot.exs b/apps/expert/bin/boot.exs deleted file mode 100644 index 551a64db..00000000 --- a/apps/expert/bin/boot.exs +++ /dev/null @@ -1,29 +0,0 @@ -script_dir = __DIR__ - -Enum.each(["consolidated", "config", "priv"], fn dir -> - [script_dir, "..", dir] - |> Path.join() - |> Code.append_path() -end) - -[script_dir, "..", "lib", "*.ez"] -|> Path.join() -|> Path.wildcard() -|> Enum.each(fn archive_path -> - lib = - archive_path - |> Path.basename() - |> String.replace_suffix(".ez", "") - - [archive_path, lib, "ebin"] - |> Path.join() - |> Code.append_path() -end) - -XPExpert.Boot.start() - -if System.get_env("XP_HALT_AFTER_BOOT") do - require Logger - Logger.warning("Shutting down (XP_HALT_AFTER_BOOT)") - System.halt() -end diff --git a/apps/expert/bin/debug_shell.sh b/apps/expert/bin/debug_shell.sh index 4d4a80c8..cbd73f28 100755 --- a/apps/expert/bin/debug_shell.sh +++ b/apps/expert/bin/debug_shell.sh @@ -5,5 +5,4 @@ node_name=$(epmd -names | grep manager-"$project_name" | awk '{print $2}') iex --name "shell@127.0.0.1" \ --remsh "${node_name}" \ - --dot-iex .iex.namespaced.exs \ --cookie expert diff --git a/apps/expert/bin/start_expert.sh b/apps/expert/bin/start_expert.sh deleted file mode 100755 index ee813daf..00000000 --- a/apps/expert/bin/start_expert.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -set -o pipefail - -script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" - -# shellcheck disable=SC1091 -if ! . "$script_dir"/activate_version_manager.sh; then - echo >&2 "Could not activate a version manager. Trying system installation." -fi - -case $1 in - iex) - elixir_command=iex - ;; - *) - elixir_command=elixir - ;; -esac - -$elixir_command \ - --cookie "expert" \ - --no-halt \ - "$script_dir/boot.exs" diff --git a/apps/expert/config/config.exs b/apps/expert/config/config.exs index 45a44d26..f94d6393 100644 --- a/apps/expert/config/config.exs +++ b/apps/expert/config/config.exs @@ -5,4 +5,12 @@ config :snowflake, # First second of 2024 epoch: 1_704_070_800_000 +case System.get_env("EXPERT_RELEASE_MODE", "plain") do + "burrito" -> + config :expert, arg_parser: {Burrito.Util.Args, :get_arguments, []} + + "plain" -> + config :expert, arg_parser: {System, :argv, []} +end + import_config("#{config_env()}.exs") diff --git a/apps/expert/lib/expert/boot.ex b/apps/expert/lib/expert/boot.ex deleted file mode 100644 index 9d5e6073..00000000 --- a/apps/expert/lib/expert/boot.ex +++ /dev/null @@ -1,148 +0,0 @@ -defmodule Expert.Boot do - @moduledoc """ - This module is called when the server starts by the start script. - - Packaging will ensure that config.exs and runtime.exs will be visible to the `:code` module - """ - alias Forge.VM.Versions - alias Future.Code - require Logger - - # halt/1 will generate a "no local return" error, which is exactly right, but that's it's _job_ - @dialyzer {:nowarn_function, halt: 1} - - @env Mix.env() - @target Mix.target() - @dep_apps Enum.map(Mix.Dep.cached(), & &1.app) - - def start do - {:ok, _} = Application.ensure_all_started(:mix) - - Application.stop(:logger) - load_config() - Application.ensure_all_started(:logger) - - Enum.each(@dep_apps, &load_app_modules/1) - - case detect_errors() do - [] -> - :ok - - errors -> - errors - |> Enum.join("\n\n") - |> halt() - end - - Application.ensure_all_started(:expert) - end - - @doc false - def detect_errors do - versioning_errors() - end - - defp load_config do - config = read_config("config.exs") - runtime = read_config("runtime.exs") - merged_config = Config.Reader.merge(config, runtime) - apply_config(merged_config) - end - - defp apply_config(configs) do - for {app_name, keywords} <- configs, - {config_key, config_value} <- keywords do - Application.put_env(app_name, config_key, config_value) - end - end - - defp read_config(file_name) do - case where_is_file(String.to_charlist(file_name)) do - {:ok, path} -> - Config.Reader.read!(path, env: @env, target: @target) - - _ -> - [] - end - end - - defp where_is_file(file_name) do - case :code.where_is_file(file_name) do - :non_existing -> - :error - - path -> - {:ok, List.to_string(path)} - end - end - - defp load_app_modules(app_name) do - Application.ensure_loaded(app_name) - modules = Application.spec(app_name, :modules) - Code.ensure_all_loaded!(modules) - end - - @allowed_elixir %{ - "1.15.0" => ">= 1.15.8", - "1.16.0" => ">= 1.16.0", - "1.17.0-rc" => ">= 1.17.0-rc", - "1.17.0" => ">= 1.17.0", - "1.18.0" => ">= 1.18.1" - } - @allowed_erlang %{ - "24" => ">= 24.3.4", - "25" => ">= 25.0.0", - "26" => ">= 26.0.2", - "27" => ">= 27.0.0" - } - - defp versioning_errors do - versions = Versions.to_versions(Versions.current()) - - elixir_base = to_string(%Version{versions.elixir | patch: 0}) - erlang_base = to_string(versions.erlang.major) - - detected_elixir_range = Map.get(@allowed_elixir, elixir_base, false) - detected_erlang_range = Map.get(@allowed_erlang, erlang_base, false) - - elixir_ok? = detected_elixir_range && Version.match?(versions.elixir, detected_elixir_range) - erlang_ok? = detected_erlang_range && Version.match?(versions.erlang, detected_erlang_range) - - errors = [ - unless elixir_ok? do - """ - FATAL: Expert is not compatible with Elixir #{versions.elixir} - - Expert is compatible with the following versions of Elixir: - - #{format_allowed_versions(@allowed_elixir)} - """ - end, - unless erlang_ok? do - """ - FATAL: Expert is not compatible with Erlang/OTP #{versions.erlang} - - Expert is compatible with the following versions of Erlang/OTP: - - #{format_allowed_versions(@allowed_erlang)} - """ - end - ] - - Enum.filter(errors, &Function.identity/1) - end - - defp format_allowed_versions(%{} = versions) do - versions - |> Map.values() - |> Enum.sort() - |> Enum.map_join("\n", fn range -> " #{range}" end) - end - - defp halt(message) do - Mix.Shell.IO.error(message) - Logger.emergency(message) - Logger.flush() - System.halt() - end -end diff --git a/apps/expert/lib/expert/engine_node.ex b/apps/expert/lib/expert/engine_node.ex index 0722491d..be2ab967 100644 --- a/apps/expert/lib/expert/engine_node.ex +++ b/apps/expert/lib/expert/engine_node.ex @@ -141,20 +141,35 @@ defmodule Expert.EngineNode do end end - @excluded_apps [:patch, :nimble_parsec] - @allowed_apps [:engine | Mix.Project.deps_apps()] -- @excluded_apps - - defp app_globs do - app_globs = Enum.map(@allowed_apps, fn app_name -> "/**/#{app_name}*/ebin" end) - ["/**/priv" | app_globs] - end + if Mix.env() == :test do + # In test environment, Expert depends on the Engine app, so we look for it + # in the expert build path. + @excluded_apps [:patch, :nimble_parsec] + @allowed_apps [:engine | Mix.Project.deps_apps()] -- @excluded_apps + + defp app_globs do + app_globs = Enum.map(@allowed_apps, fn app_name -> "/**/#{app_name}*/ebin" end) + ["/**/priv" | app_globs] + end - def glob_paths do - for entry <- :code.get_path(), - entry_string = List.to_string(entry), - entry_string != ".", - Enum.any?(app_globs(), &PathGlob.match?(entry_string, &1, match_dot: true)) do - entry + def glob_paths do + for entry <- :code.get_path(), + entry_string = List.to_string(entry), + entry_string != ".", + Enum.any?(app_globs(), &PathGlob.match?(entry_string, &1, match_dot: true)) do + entry + end + end + else + # In dev and prod environments, a default build of Engine is built + # separately and copied to expert's priv directory. + # When Engine is built in CI for a version matrix, we'll need to check if + # we have the right version downloaded, and if not, we should download it. + defp glob_paths do + :expert + |> :code.priv_dir() + |> Path.join("lib/**/ebin") + |> Path.wildcard() end end diff --git a/apps/expert/lib/expert/iex/helpers.ex b/apps/expert/lib/expert/iex/helpers.ex deleted file mode 100644 index 270d0b6c..00000000 --- a/apps/expert/lib/expert/iex/helpers.ex +++ /dev/null @@ -1,242 +0,0 @@ -defmodule Expert.IEx.Helpers do - alias Expert.CodeIntelligence - alias Expert.EngineApi - alias Forge.Ast - alias Forge.Document - alias Forge.Document.Position - alias Forge.Project - alias GenLSP.Enumerations.CompletionTriggerKind - alias GenLSP.Structures - - defmacro __using__(_) do - quote do - alias Forge.Document - alias Forge.Document.Position - - import unquote(__MODULE__) - end - end - - def observer do - # credo:disable-for-next-line - apply(:observer, :start, []) - end - - def observer(project) do - project - |> ensure_project() - |> EngineApi.call(:observer, :start) - end - - def doc(text) do - doc(:expert, text) - end - - def project_node(name) do - name - |> project() - |> Project.node_name() - end - - def doc(project, text) do - root_path = - project - |> project() - |> Project.root_path() - - [root_path, "lib", "file.ex"] - |> Path.join() - |> Document.Path.to_uri() - |> Document.new(text, 0) - end - - def pos(doc, line, character) do - Position.new(doc, line, character) - end - - def compile_project(project) do - project - |> ensure_project() - |> EngineApi.schedule_compile(true) - end - - def compile_file(project, source) when is_binary(source) do - project - |> ensure_project() - |> compile_file(doc(source)) - end - - def compile_file(project, %Document{} = document) do - project - |> ensure_project() - |> EngineApi.compile_document(document) - end - - def complete(project, source, context \\ nil) - - def complete(project, source, context) when is_binary(source) do - case completion_position(source) do - {:found, line, character} -> - analysis = source |> doc() |> Ast.analyze() - complete(project, analysis, line, character, context) - - other -> - other - end - end - - def complete(project, %Ast.Analysis{} = analysis, line, character, context) do - context = - if is_nil(context) do - %Structures.CompletionContext{trigger_kind: CompletionTriggerKind.trigger_character()} - else - context - end - - position = pos(analysis.document, line, character) - - project - |> ensure_project() - |> CodeIntelligence.Completion.complete(analysis, position, context) - end - - def connect do - manager_name = manager_name() - Node.start(:"r@127.0.0.1") - Node.set_cookie(:expert) - Node.connect(:"#{manager_name}@127.0.0.1") - end - - @doc """ - Create an Expert Project for an application in the same directory as - Expert. - - Alternatively, a project for one of our test fixtures can be created - using the `fixture: true` option. - - ## Examples - - iex> project() - %Forge.Project{ - root_uri: "file:///.../expert - ... - } - - iex> project(:my_project) - %Forge.Project{ - root_uri: "file:///.../my_project" - ... - } - - iex> project(:navigations, fixture: true) - %Forge.Project{ - root_uri: "file:///.../expert/apps/engine/test/fixtures/navigations" - ... - } - - """ - def project(project \\ :expert, opts \\ []) do - project = - if opts[:fixture] do - "expert/apps/engine/test/fixtures/#{project}" - else - project - end - - # We're using a cache here because we need project's - # entropy to be the same after every call. - trans(project, fn -> - project_path = - [File.cwd!(), "..", to_string(project)] - |> Path.join() - |> Path.expand() - - project_uri = "file://#{project_path}" - Forge.Project.new(project_uri) - end) - end - - def current_project do - [prefix, _] = Node.self() |> to_string() |> String.split("@") - [_, project_name, entropy] = String.split(prefix, "-") - %{ensure_project(project_name) | entropy: entropy} - end - - def stop_project(project) do - project - |> ensure_project() - |> Expert.Project.Supervisor.stop() - end - - def start_project(project) do - project - |> ensure_project() - |> Expert.Project.Supervisor.start() - end - - def time(fun) when is_function(fun, 0) do - {elapsed_us, result} = :timer.tc(fun) - - IO.puts([ - IO.ANSI.format([:cyan, :bright, "Time: "]), - Forge.Formats.time(elapsed_us) - ]) - - result - end - - defp manager_name do - {:ok, names} = :erl_epmd.names() - - names - |> Enum.map(fn {name, _port} -> List.to_string(name) end) - |> Enum.find(&String.starts_with?(&1, "manager")) - end - - defp completion_position(source_string) do - source_string - |> String.split(["\r\n", "\r", "\n"]) - |> Enum.with_index() - |> Enum.reduce_while(:not_found, fn {line, line_number}, _ -> - if String.contains?(line, "|") do - index = - line - |> String.graphemes() - |> Enum.find_index(&(&1 == "|")) - - {:halt, {:found, line_number + 1, index + 1}} - else - {:cont, :not_found} - end - end) - end - - defp ensure_project(%Project{} = project) do - project - end - - defp ensure_project(project) when is_binary(project) do - project - |> String.to_atom() - |> project() - end - - defp ensure_project(project) when is_atom(project) do - project(project) - end - - defp trans(name, function) do - name = {__MODULE__, name} - - case :persistent_term.get(name, :undefined) do - :undefined -> - value = function.() - :persistent_term.put(name, value) - - _ -> - :ok - end - - :persistent_term.get(name) - end -end diff --git a/apps/expert/lib/expert/release.ex b/apps/expert/lib/expert/release.ex new file mode 100644 index 00000000..676f20a3 --- /dev/null +++ b/apps/expert/lib/expert/release.ex @@ -0,0 +1,21 @@ +defmodule Expert.Release do + def assemble(release) do + Mix.Task.run(:namespace, [release.path]) + + engine_path = Path.expand("../../../engine", __DIR__) + + source = Path.join([engine_path, "_build/dev_ns"]) + + dest = + Path.join([ + release.path, + "lib", + "xp_expert-#{release.version}", + "priv" + ]) + + File.cp_r!(source, dest) + + release + end +end diff --git a/apps/expert/lib/mix/tasks/package.ex b/apps/expert/lib/mix/tasks/package.ex deleted file mode 100644 index d24a9764..00000000 --- a/apps/expert/lib/mix/tasks/package.ex +++ /dev/null @@ -1,339 +0,0 @@ -defmodule Mix.Tasks.Package do - @moduledoc """ - Creates the Expert application's artifacts - - Expert does some strange things to its own code via namespacing, but because it does so, we can't - use standard build tooling. The app names of its modules and dependencies are changed, so `mix install` - won't realize that the correct apps are installed. It uses two different VMs, so making an `escript` - will fail, as the second VM needs to find Expert's modules _somewhere. Releases seem ideal, and we - used them for a while, but they need to match the _exact_ Elixir and Erlang versions they were compiled on, - right down to the patch level. This is much, much too strict for a project that needs to be able to run - on a variety of elixir / erlang versions. - - An ideal packaging system will have the following properties: - - * It creates a self contained artifact directory - * It will run under a variety of versions of Elixir and Erlang - * It allows namespaced apps to run - * It allows multiple VMs to find elixir's modules - * It allows us to package non-elixir resources (mainly launcher scripts) and access them during runtime - - This packaging system meets all of the above parameters. - It works by examining the server app's dependencies, namespacing what's required and then packaging them - in to [Erlang archive files](https://www.erlang.org/doc/man/code.html#loading-of-code-from-archive-files) - in a `lib` directory. Similarly, the configuration is copied to a `config` directory, and the launcher - scripts are copied to a `bin` directory. - - The end result is a release-like filesystem, but without a lot of the erlang booting stuff. Bootstrapping - accomplished by simple scripts that reside in the project's `/bin` directory, and the `Expert.Boot` - module, which loads applications and their modules. - - ## Command line options - - * `--path` - The package will be written to the path given. Defaults to `./build/dev/expert`. If the - `--zip` option is specified, The name of the zip file is determined by the last entry of the path. - For example, if the `--path` option is `_build/dev/output`, then the name of the zip file will be - `output.zip`. - * `--zip` - The resulting package will be zipped. The zip file will be placed in the current directory, - and the package directory will be deleted - - - ## Directory structure - ```text - bin/ - start_expert.sh - debug_shell.sh - lib/ - xp_forge.sh - xp_engine.sh - xp_expert.ez - ... - config/ - config.exs - dev.exs - prod.exs - test.exs - runtime.exs - priv/ - port_wrapper.sh - ... - consolidated/ - Elixir.(consolidated protocol module).beam - ``` - - On boot, the `ERL_LIBS` environment variable is set to the `lib` directory so all of the `.ez` files are - picked up by the code server. Similarly, the `config`, `consolidated` and `priv` directories are added - to the code search path with the `-pa` argument. - """ - - alias Forge.Namespace - alias Forge.VM.Versions - - @options [ - strict: [ - path: :string, - zip: :boolean - ] - ] - - @execute_permisson 0o755 - - def run(args) do - {opts, _, _} = OptionParser.parse(args, @options) - default_path = Path.join([Mix.Project.build_path(), "package", "expert"]) - package_root = Keyword.get(opts, :path, default_path) - - rebuild_on_version_change(package_root) - - Mix.Task.run(:compile) - Mix.Shell.IO.info("Assembling build in #{package_root}") - File.mkdir_p!(package_root) - - {:ok, scratch_directory} = prepare(package_root) - - build_archives(package_root, scratch_directory) - copy_consolidated_beams(package_root) - copy_launchers(package_root) - copy_priv_files(package_root) - copy_config(package_root) - write_vm_versions(package_root) - - File.rm_rf!(scratch_directory) - - if Keyword.get(opts, :zip, false) do - zip(package_root) - File.rm_rf(package_root) - end - end - - defp rebuild_on_version_change(package_root) do - %{elixir: elixir_current, erlang: erlang_current} = Versions.current() - - with {:ok, %{elixir: elixir_compiled, erlang: erlang_compiled}} <- - Versions.read(priv_path(package_root)) do - if elixir_compiled != elixir_current or erlang_compiled != erlang_current do - Code.put_compiler_option(:ignore_module_conflict, true) - Mix.Shell.IO.error("The version of elixir or erlang has changed. Forcing recompilation.") - File.rm_rf!(package_root) - Mix.Task.clear() - Mix.Task.run(:clean, ~w(--deps)) - end - end - end - - defp prepare(package_root) do - scratch_directory = Path.join(package_root, "scratch") - File.mkdir(scratch_directory) - - [Mix.Project.build_path(), "lib"] - |> Path.join() - |> File.cp_r!(Path.join(scratch_directory, "lib")) - - Mix.Task.run(:namespace, [scratch_directory]) - {:ok, scratch_directory} - end - - defp build_archives(package_root, scratch_directory) do - scratch_directory - |> target_path() - |> File.mkdir_p!() - - app_dirs = app_dirs(scratch_directory) - - Enum.each(app_dirs, fn {app_name, path} -> - create_archive(package_root, app_name, path) - end) - end - - defp app_dirs(scratch_directory) do - lib_directory = Path.join(scratch_directory, "lib") - server_deps = server_deps() - - lib_directory - |> File.ls!() - |> Enum.filter(&(&1 in server_deps)) - |> Map.new(fn dir -> - app_name = Path.basename(dir) - {app_name, Path.join([scratch_directory, "lib", dir])} - end) - end - - defp create_archive(package_root, app_name, app_path) do - file_list = file_list(app_name, app_path) - zip_path = Path.join([target_path(package_root), "#{app_name}.ez"]) - - {:ok, _} = - zip_path - |> String.to_charlist() - |> :zip.create(file_list, uncompress: [~c".beam"]) - - :ok - end - - defp file_list(app_name, app_path) do - File.cd!(app_path, fn -> - beams = Path.wildcard("ebin/*.{app,beam}") - priv = Path.wildcard("priv/**/*", match_dot: true) - - Enum.reduce(beams ++ priv, [], fn relative_path, acc -> - case File.read(relative_path) do - {:ok, contents} -> - zip_relative_path = - app_name - |> Path.join(relative_path) - |> String.to_charlist() - - [{zip_relative_path, contents} | acc] - - {:error, _} -> - acc - end - end) - end) - end - - defp copy_consolidated_beams(package_root) do - beams_dest_dir = Path.join(package_root, "consolidated") - - File.mkdir_p!(beams_dest_dir) - - File.cp_r!(Mix.Project.consolidation_path(), beams_dest_dir) - - beams_dest_dir - |> File.ls!() - |> Enum.each(fn relative_path -> - absolute_path = Path.join(beams_dest_dir, relative_path) - Namespace.Transform.Beams.apply(absolute_path) - end) - end - - defp copy_launchers(package_root) do - launcher_source_dir = - Mix.Project.project_file() - |> Path.dirname() - |> Path.join("bin") - - launcher_dest_dir = Path.join(package_root, "bin") - - File.mkdir_p!(launcher_dest_dir) - File.cp_r!(launcher_source_dir, launcher_dest_dir) - - launcher_dest_dir - |> Path.join("*") - |> Path.wildcard() - |> Enum.each(fn path -> - File.chmod!(path, @execute_permisson) - end) - end - - defp target_path(scratch_directory) do - Path.join([scratch_directory, "lib"]) - end - - defp server_deps do - deps_apps = - if Mix.Project.get() == Expert.MixProject do - Mix.Project.deps_apps() - else - server_path = Mix.Project.deps_paths()[:expert] - - Mix.Project.in_project(:expert, server_path, fn _ -> - Mix.Project.deps_apps() - end) - end - - deps = - Enum.map(deps_apps, fn app_module -> - app_module - |> Namespace.Module.apply() - |> to_string() - end) - - server_dep = - :expert - |> Namespace.Module.apply() - |> to_string() - - [server_dep | deps] - end - - defp copy_config(package_root) do - config_source = - Mix.Project.config()[:config_path] - |> Path.absname() - |> Path.dirname() - - config_dest = Path.join(package_root, "config") - File.mkdir_p!(config_dest) - File.cp_r!(config_source, config_dest) - - Namespace.Transform.Configs.apply_to_all(config_dest) - end - - @priv_apps [:expert] - - defp copy_priv_files(package_root) do - priv_dest_dir = priv_path(package_root) - - Enum.each(@priv_apps, fn app_name -> - case priv_dir(app_name) do - {:ok, priv_source_dir} -> - File.cp_r!(priv_source_dir, priv_dest_dir) - - _ -> - :ok - end - end) - end - - defp write_vm_versions(package_root) do - package_root - |> priv_path() - |> Versions.write() - end - - defp zip(package_root) do - package_name = Path.basename(package_root) - - zip_output = Path.join(File.cwd!(), "#{package_name}.zip") - - package_root - |> Path.dirname() - |> File.cd!(fn -> - System.cmd("zip", ["-r", zip_output, package_name]) - end) - end - - defp priv_dir(app) do - case :code.priv_dir(app) do - {:error, _} -> - :error - - path -> - normalized = - path - |> List.to_string() - |> normalize_path() - - {:ok, normalized} - end - end - - defp normalize_path(path) do - case File.read_link(path) do - {:ok, orig} -> - path - |> Path.dirname() - |> Path.join(orig) - |> Path.expand() - |> Path.absname() - - _ -> - path - end - end - - defp priv_path(package_root) do - Path.join(package_root, "priv") - end -end diff --git a/apps/expert/mix.exs b/apps/expert/mix.exs index 7722eb23..63a28cfe 100644 --- a/apps/expert/mix.exs +++ b/apps/expert/mix.exs @@ -11,13 +11,15 @@ defmodule Expert.MixProject do deps: deps(), dialyzer: Mix.Dialyzer.config(add_apps: [:jason]), aliases: aliases(), - elixirc_paths: elixirc_paths(Mix.env()) + elixirc_paths: elixirc_paths(Mix.env()), + releases: releases(), + default_release: :expert ] end def application do [ - extra_applications: [:logger, :runtime_tools, :kernel, :erts], + extra_applications: [:logger, :runtime_tools, :kernel, :erts, :observer], mod: {Expert.Application, []} ] end @@ -38,11 +40,46 @@ defmodule Expert.MixProject do ["lib"] end + defp releases() do + [ + expert: [ + strip_beams: false, + cookie: "expert", + steps: release_steps() ++ [&Burrito.wrap/1], + burrito: [ + targets: [ + darwin_arm64: [os: :darwin, cpu: :aarch64], + darwin_amd64: [os: :darwin, cpu: :x86_64], + linux_arm64: [os: :linux, cpu: :aarch64], + linux_amd64: [os: :linux, cpu: :x86_64], + windows_amd64: [os: :windows, cpu: :x86_64] + ] + ] + ], + plain: [ + strip_beams: false, + cookie: "expert", + steps: release_steps() + ] + ] + end + + defp release_steps() do + [ + :assemble, + &Expert.Release.assemble/1 + ] + end + defp deps do [ + {:burrito, "~> 1.3", only: [:dev, :prod]}, Mix.Credo.dependency(), Mix.Dialyzer.dependency(), - {:engine, path: "../engine", env: Mix.env()}, + # In practice Expert does not hardly depend on Engine, only on its compiled + # artifacts, but we need it as a test dependency to set up tests that + # assume a roundtrip to a project node is made. + {:engine, path: "../engine", env: Mix.env(), only: [:test]}, {:forge, path: "../forge", env: Mix.env()}, {:gen_lsp, "~> 0.11"}, {:jason, "~> 1.4"}, diff --git a/apps/expert/mix.lock b/apps/expert/mix.lock index ea4f538f..9829ccf2 100644 --- a/apps/expert/mix.lock +++ b/apps/expert/mix.lock @@ -1,20 +1,29 @@ %{ "benchee": {:hex, :benchee, "1.3.1", "c786e6a76321121a44229dde3988fc772bca73ea75170a73fd5f4ddf1af95ccf", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "76224c58ea1d0391c8309a8ecbfe27d71062878f59bd41a390266bf4ac1cc56d"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "burrito": {:hex, :burrito, "1.3.0", "4be8504185250756ff4a8770d0c0d91dbfe518d2faa5f1888f13b00540028c59", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, ">= 0.5.0", [hex: :req, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.2.0 or ~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "a53f6bc0644bfd998164d68714c9af04291c220f5f7d0c90cb9616780cc60165"}, "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "73ce7e0d239342fb9527d7ba567203e77dbb9b25", [ref: "73ce7e0d239342fb9527d7ba567203e77dbb9b25"]}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, + "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, "gen_lsp": {:hex, :gen_lsp, "0.11.0", "9eda4d2fcaff94d9b3062e322fcf524c176db1502f584a3cff6135088b46084b", [:mix], [{:jason, "~> 1.3", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:schematic, "~> 0.2.1", [hex: :schematic, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "d67c20650a5290a02f7bac53083ac4487d3c6b461f35a8b14c5d2d7638c20d26"}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "logger_file_backend": {:hex, :logger_file_backend, "0.0.14", "774bb661f1c3fed51b624d2859180c01e386eb1273dc22de4f4a155ef749a602", [:mix], [], "hexpm", "071354a18196468f3904ef09413af20971d55164267427f6257b52cfba03f9e6"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "observer_cli": {:hex, :observer_cli, "1.8.3", "866ee083eb3482d5f40d301c2ac7e1df0b6061d02ae771e164d71931d3c687c4", [:mix, :rebar3], [{:recon, "~> 2.5.6", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "041c638d54cc8265e6e0472aec7c17a83bb2c4a02628ddedd9138747d9d0b8bf"}, "patch": {:hex, :patch, "0.15.0", "947dd6a8b24a2d2d1137721f20bb96a8feb4f83248e7b4ad88b4871d52807af5", [:mix], [], "hexpm", "e8dadf9b57b30e92f6b2b1ce2f7f57700d14c66d4ed56ee27777eb73fb77e58d"}, "path_glob": {:hex, :path_glob, "0.2.0", "b9e34b5045cac5ecb76ef1aa55281a52bf603bf7009002085de40958064ca312", [:mix], [{:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "be2594cb4553169a1a189f95193d910115f64f15f0d689454bb4e8cfae2e7ebc"}, + "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, "refactorex": {:hex, :refactorex, "0.1.52", "22a69062c84e0f20a752d3d6580269c09c242645ee4f722f03d4270dd8cbf218", [:mix], [{:sourceror, "~> 1.7", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "4927fe6c3acd1f4695d6d3e443380167d61d004d507b1279c6084433900c94d0"}, + "req": {:hex, :req, "0.5.14", "521b449fa0bf275e6d034c05f29bec21789a0d6cd6f7a1c326c7bee642bf6e07", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "b7b15692071d556c73432c7797aa7e96b51d1a2db76f746b976edef95c930021"}, "schematic": {:hex, :schematic, "0.2.1", "0b091df94146fd15a0a343d1bd179a6c5a58562527746dadd09477311698dbb1", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0b255d65921e38006138201cd4263fd8bb807d9dfc511074615cd264a571b3b1"}, "snowflake": {:hex, :snowflake, "1.0.4", "8433b4e04fbed19272c55e1b7de0f7a1ee1230b3ae31a813b616fd6ef279e87a", [:mix], [], "hexpm", "badb07ebb089a5cff737738297513db3962760b10fe2b158ae3bebf0b4d5be13"}, "sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"}, diff --git a/apps/expert/test/expert/boot_test.exs b/apps/expert/test/expert/boot_test.exs deleted file mode 100644 index 61765003..00000000 --- a/apps/expert/test/expert/boot_test.exs +++ /dev/null @@ -1,58 +0,0 @@ -defmodule Expert.BootTest do - alias Expert.Boot - alias Forge.VM.Versions - - use ExUnit.Case - use Patch - - describe "detect_errors/0" do - test "returns empty list when all checks succeed" do - patch_runtime_versions("1.15.8", "25.0") - patch_compiled_versions("1.15.8", "25.0") - - assert [] = Boot.detect_errors() - end - - test "includes error when runtime elixir is incompatible" do - patch_runtime_versions("1.12.0", "24.3.4") - patch_compiled_versions("1.13.4", "24.3.4") - - assert [error] = Boot.detect_errors() - assert error =~ "FATAL: Expert is not compatible with Elixir 1.12.0" - end - - test "includes error when runtime erlang is incompatible" do - patch_runtime_versions("1.15.8", "23.0") - patch_compiled_versions("1.15.8", "23.0") - - assert [error] = Boot.detect_errors() - assert error =~ "FATAL: Expert is not compatible with Erlang/OTP 23.0.0" - end - - test "includes multiple errors when runtime elixir and erlang are incompatible" do - patch_runtime_versions("1.15.2", "26.0.0") - patch_compiled_versions("1.15.8", "26.1") - - assert [elixir_error, erlang_error] = Boot.detect_errors() - assert elixir_error =~ "FATAL: Expert is not compatible with Elixir 1.15.2" - assert erlang_error =~ "FATAL: Expert is not compatible with Erlang/OTP 26.0.0" - end - end - - defp patch_runtime_versions(elixir, erlang) do - patch(Versions, :elixir_version, elixir) - patch(Versions, :erlang_version, erlang) - end - - defp patch_compiled_versions(elixir, erlang) do - patch(Versions, :code_find_file, fn file -> {:ok, file} end) - - patch(Versions, :read_file, fn file -> - if String.ends_with?(file, ".elixir") do - {:ok, elixir} - else - {:ok, erlang} - end - end) - end -end diff --git a/apps/forge/lib/forge/build.ex b/apps/forge/lib/forge/build.ex new file mode 100644 index 00000000..34727750 --- /dev/null +++ b/apps/forge/lib/forge/build.ex @@ -0,0 +1,22 @@ +defmodule Mix.Tasks.Build do + use Mix.Task + + def run(_args) do + Mix.Task.run("compile", []) + + namespaced_dir = "_build/#{Mix.env()}_ns" + + # Remove the existing namespaced dir + File.rm_rf(namespaced_dir) + # Create our namespaced area + File.mkdir_p(namespaced_dir) + + # Move our build artifacts from safekeeping to the build area + File.cp_r!("_build/#{Mix.env()}", namespaced_dir) + + # Namespace the new code + Mix.Task.run(:namespace, [ + namespaced_dir + ]) + end +end diff --git a/flake.lock b/flake.lock index 81d1605c..66aafe4f 100644 --- a/flake.lock +++ b/flake.lock @@ -48,7 +48,9 @@ "inputs": { "flake-parts": "flake-parts", "nixpkgs": "nixpkgs", - "systems": "systems" + "systems": "systems", + "xzpkgs": "xzpkgs", + "zigpkgs": "zigpkgs" } }, "systems": { @@ -65,6 +67,38 @@ "repo": "default", "type": "github" } + }, + "xzpkgs": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + } + }, + "zigpkgs": { + "locked": { + "lastModified": 1747426788, + "narHash": "sha256-N4cp0asTsJCnRMFZ/k19V9akkxb7J/opG+K+jU57JGc=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "12a55407652e04dcf2309436eb06fef0d3713ef3", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "12a55407652e04dcf2309436eb06fef0d3713ef3", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index ef1e4d81..4d36b230 100644 --- a/flake.nix +++ b/flake.nix @@ -2,12 +2,16 @@ description = "Reimagined language server for Elixir"; inputs.nixpkgs.url = "flake:nixpkgs"; + inputs.zigpkgs.url = "github:nixos/nixpkgs/12a55407652e04dcf2309436eb06fef0d3713ef3"; + inputs.xzpkgs.url = "github:nixos/nixpkgs/18dd725c29603f582cf1900e0d25f9f1063dbf11"; inputs.flake-parts.url = "github:hercules-ci/flake-parts"; inputs.systems.url = "github:nix-systems/default"; outputs = { self, systems, + zigpkgs, + xzpkgs, ... } @ inputs: inputs.flake-parts.lib.mkFlake {inherit inputs;} { @@ -19,8 +23,14 @@ systems = import systems; - perSystem = {pkgs, ...}: let + perSystem = { + pkgs, + system, + ... + }: let erlang = pkgs.beam.packages.erlang_25; + zpkgs = zigpkgs.legacyPackages.${system}; + xzpkgs' = xzpkgs.legacyPackages.${system}; expert = self.lib.mkExpert {inherit erlang;}; in { formatter = pkgs.alejandra; @@ -51,17 +61,14 @@ }; devShells.default = pkgs.mkShell { - packages = let - beamPackages = pkgs.beam.packages; - in - [ - beamPackages.erlang_27.erlang - beamPackages.erlang_27.elixir_1_17 - ] - ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ - pkgs.darwin.apple_sdk.frameworks.CoreFoundation - pkgs.darwin.apple_sdk.frameworks.CoreServices - ]; + packages = with pkgs; [ + beam.packages.erlang_27.erlang + beam.packages.erlang_27.elixir_1_17 + zpkgs.zig_0_14 + xzpkgs'.xz + just + _7zz + ]; }; }; }; diff --git a/integration/Dockerfile b/integration/Dockerfile deleted file mode 100644 index e9e91258..00000000 --- a/integration/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -# Used to build a clean container for testing the Expert boot sequence. -# -# Build: docker build -t xp -f integration/Dockerfile . -# Run: docker run -it xp - -ARG SYS_ELIXIR_VERSION=1.15.7 -ARG SYS_ERLANG_VERSION=26.2.1 - -FROM hexpm/elixir:${SYS_ELIXIR_VERSION}-erlang-${SYS_ERLANG_VERSION}-ubuntu-jammy-20231128 - -ENV ELIXIR_VERSION=1.15.7-otp-26 -ENV ERLANG_VERSION=26.2.1 - -RUN apt-get update -RUN apt-get install -y \ - git \ - curl \ - unzip \ - # used to compile erlang - build-essential \ - autoconf \ - openssl \ - libssl-dev - -WORKDIR /expert - -COPY integration/boot/set_up_mise.sh integration/boot/set_up_mise.sh -RUN integration/boot/set_up_mise.sh - -COPY integration/boot/set_up_asdf.sh integration/boot/set_up_asdf.sh -RUN integration/boot/set_up_asdf.sh - -COPY mix_*.exs . -COPY apps apps - -WORKDIR /expert/apps/expert - -RUN mix local.hex --force -RUN mix deps.get -RUN mix compile -RUN mix package - -CMD bash diff --git a/integration/README.md b/integration/README.md deleted file mode 100644 index 812a1d1b..00000000 --- a/integration/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Integration tests - -These integration tests confirm that Lexical can start using the correct version manager (asdf/rtx) and Elixir/Erlang versions in a variety of environments. -They work using a Docker image with both asdf and rtx installed, but with neither activated. -An individual test will then run some setup and manipulate the environment, then ensure that Lexical can properly start. - -## Running the tests - -Tests are run using `test.sh`: - -```sh -$ ./integration/test.sh -``` - -By default, this will first build the Docker image and then run the tests. -The first run will take quite some time to build the image, but subsequent runs will be much faster, as most setup will be cached and only Lexical will need to be rebuilt. - -If needed, you can separate building and running using `build.sh` and the `NO_BUILD=1` flag: - -```sh -$ ./integration/build.sh -$ NO_BUILD=1 ./integration/test.sh -``` - -### Debugging - -Run the tests with `XP_DEBUG=1` to see the output from the underlying commands: - -```sh -$ XP_DEBUG=1 ./integration/test.sh -... -test_find_asdf_directory... -> No version manager detected -> Found asdf. Activating... -> Detected Elixir through asdf: /root/.asdf/installs/elixir/1.15.8-otp-26/bin/elixir -Pass -... -``` diff --git a/integration/boot/set_up_asdf.sh b/integration/boot/set_up_asdf.sh deleted file mode 100755 index ae75c810..00000000 --- a/integration/boot/set_up_asdf.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail - -asdf_dir=/version_managers/asdf_vm -mkdir -p $asdf_dir && cd $asdf_dir - -git clone https://github.com/asdf-vm/asdf.git . - -# shellcheck disable=SC1091 -ASDF_DIR=$asdf_dir . asdf.sh - -export KERL_CONFIGURE_OPTIONS="--disable-debug --without-javac --without-termcap --without-wx" -asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git -asdf install erlang "$ERLANG_VERSION" -asdf global erlang "$ERLANG_VERSION" - -asdf plugin add elixir https://github.com/asdf-vm/asdf-elixir.git -asdf install elixir "$ELIXIR_VERSION" -asdf global elixir "$ELIXIR_VERSION" diff --git a/integration/boot/set_up_mise.sh b/integration/boot/set_up_mise.sh deleted file mode 100755 index 52a9c8bc..00000000 --- a/integration/boot/set_up_mise.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail - -mise_dir=/version_managers/mise_vm -mkdir -p $mise_dir && cd $mise_dir - -# Download the mise binary for the correct architecture -arch=$(uname -m) -architecture="" - -case $arch in - "x86_64") - architecture="linux-x64" - ;; - "aarch64") - architecture="linux-arm64" - ;; - "arm64") - architecture="macos-arm64" - ;; - *) - echo "Unsupported architecture: $arch" - exit 1 - ;; -esac - -curl -L "https://github.com/jdx/mise/releases/download/v2025.2.6/mise-v2025.2.6-${architecture}.tar.gz" -o mise.tar.gz -tar xfvz mise.tar.gz -mv mise mise_download -mv mise_download/bin/mise . -chmod +x ./mise - -eval "$(./mise activate bash)" -export MISE_VERBOSE=1 -export KERL_CONFIGURE_OPTIONS="--disable-debug --without-javac --without-termcap --without-wx" - -./mise use --global "erlang@$ERLANG_VERSION" -./mise use --global "elixir@$ELIXIR_VERSION" diff --git a/integration/build.sh b/integration/build.sh deleted file mode 100755 index 6954dfed..00000000 --- a/integration/build.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail - -docker build -t xp -f integration/Dockerfile . diff --git a/integration/test.sh b/integration/test.sh deleted file mode 100755 index 9e11b4ba..00000000 --- a/integration/test.sh +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env bash - -# Disable warning for interpolations in single quotes: -# shellcheck disable=2016 - -set -eo pipefail - -script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" - -# shellcheck disable=SC1091 -. "$script_dir/test_utils.sh" - -# Ensure the Docker image is up-to-date unless NO_BUILD=1 -if [ -z "$NO_BUILD" ]; then - "$script_dir"/build.sh -fi - -start_expert() { - local command='XP_HALT_AFTER_BOOT=1 _build/dev/package/expert/bin/start_expert.sh; exit $?' - - if [[ $1 != "" ]]; then - command="$1 && $command" - fi - - docker run -i xp bash -c "$command" 2>&1 - return $? -} - -run_test() { - local setup=$1 - local expected=("${@:2}") - - # $FUNCNAME is a special array containing the stack of function calls, - # with the current function at the head. - local test="${FUNCNAME[1]}" - log "$test... " - - local output - output=$(start_expert "$setup") - local exit_code=$? - - if [[ -n $XP_DEBUG ]]; then - log_info "\n$(prefix_lines "> " "$output")" - fi - - assert_contains "$output" "${expected[@]}" - local assert_exit_code=$? - - if [ $exit_code -ne 0 ]; then - log_error "Fail (exit_code=$exit_code)" - return 1 - elif [ $assert_exit_code -ne 0 ]; then - return 1 - else - log_success "Pass" - fi -} - -test_system_installation() { - local expect=( - "No activated version manager detected" - "Could not activate a version manager" - ) - - run_test "" "${expect[@]}" - return $? -} - -test_asdf_already_activated() { - local setup=( - 'mv "$(which elixir)" "$(which elixir).hidden" && ' - 'ASDF_DIR=/version_managers/asdf_vm . /version_managers/asdf_vm/asdf.sh' - ) - local expect=( - "Detected Elixir through asdf" - ) - - run_test "${setup[*]}" "${expect[@]}" - return $? -} - -test_asdf_dir_found() { - local setup=( - 'mv "$(which elixir)" "$(which elixir).hidden" && ' - 'export ASDF_DIR=/version_managers/asdf_vm' - ) - local expect=( - "No activated version manager detected" - "Found asdf. Activating" - "Detected Elixir through asdf" - ) - - run_test "${setup[*]}" "${expect[@]}" - return $? -} - -test_asdf_used_when_activated_mise_missing_elixir() { - local setup=( - 'mv "$(which elixir)" "$(which elixir).hidden" && ' - 'eval "$(/version_managers/mise_vm/mise activate bash)" && ' - 'mise uninstall "elixir@$ELIXIR_VERSION" && ' - 'export ASDF_DIR=/version_managers/asdf_vm' - ) - local expect=( - "No activated version manager detected" - "Found asdf. Activating" - "Detected Elixir through asdf" - ) - - run_test "${setup[*]}" "${expect[@]}" - return $? -} - -test_mise_already_activated() { - local setup=( - 'mv "$(which elixir)" "$(which elixir).hidden" && ' - 'eval "$(/version_managers/mise_vm/mise activate bash)"' - ) - local expect=( - "Detected Elixir through mise" - ) - - run_test "${setup[*]}" "${expect[@]}" - return $? -} - -test_mise_binary_found_and_activated() { - local setup=( - 'mv "$(which elixir)" "$(which elixir).hidden" && ' - 'export PATH="/version_managers/mise_vm:$PATH"' - ) - local expect=( - "No activated version manager detected" - "Found mise" - "Detected Elixir through mise" - ) - - run_test "${setup[*]}" "${expect[@]}" - return $? -} - -test_mise_used_when_activated_asdf_missing_elixir() { - local setup=( - 'mv "$(which elixir)" "$(which elixir).hidden" && ' - 'ASDF_DIR=/version_managers/asdf_vm . /version_managers/asdf_vm/asdf.sh && ' - 'asdf uninstall elixir "$ELIXIR_VERSION" && ' - 'export PATH="/version_managers/mise_vm:$PATH"' - ) - local expect=( - "No activated version manager detected" - "Found mise. Activating" - "Detected Elixir through mise" - ) - - run_test "${setup[*]}" "${expect[@]}" - return $? -} - -run_tests_and_exit diff --git a/integration/test_utils.sh b/integration/test_utils.sh deleted file mode 100755 index 6f38c4dc..00000000 --- a/integration/test_utils.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -reset="\033[0m" -faint="$reset\033[0;2m" -red="$reset\033[0;31m" -green="$reset\033[0;32m" -cyan="$reset\033[0;36m" - -# Asserts that the given string contains all given substrings. -# -# assert_contains STRING SUBSTRING* -# -# Example: -# -# assert_contains "foobar" "foo" "bar" -# -assert_contains() { - local output=$1 - local expectations=("${@:2}") - local not_found=() - - for expected in "${expectations[@]}"; do - if [[ $output != *"$expected"* ]]; then - not_found+=("$expected") - fi - done - - if [ ${#not_found[@]} -ne 0 ]; then - log_error "Assertion failed!" - log_section "Expected" "${not_found[@]}" - log_section "To be in" "$output" - log "\n\n" - return 1 - fi -} - -# Runs every function that starts with "test_" in the current script, -# exits 1 if any tests exit non-zero -# -# Example: -# -# test_foo() { ... } -# test_bar() { ... } -# -# # automatically runs test_foo and test_bar -# run_tests_and_exit -# -run_tests_and_exit() { - local tests - tests=$(declare -F | awk '/test_/ {print $3}') - - local exit_code=0 - - for test in $tests; do - if ! "$test"; then - exit_code=1 - fi - done - - exit $exit_code -} - -log() { - echo -ne "$1" -} - -log_error() { - log "${red}$1${reset}\n" -} - -log_success() { - log "${green}$1${reset}\n" -} - -log_info() { - log "${faint}$1${reset}\n" -} - -log_section() { - local title="$1" - local content=("${@:2}") - - log "\n ${cyan}${title}:${reset}\n\n" - - for item in "${content[@]}"; do - prefix_lines " " "$item" - done -} - -prefix_lines() { - local prefix_with=$1 - echo "$2" | sed "s/^/$prefix_with/" | cat -} diff --git a/justfile b/justfile new file mode 100644 index 00000000..2b8c48b7 --- /dev/null +++ b/justfile @@ -0,0 +1,102 @@ +os := if os() == "macos" { "darwin" } else { os() } +arch := if arch() =~ "(arm|aarch64)" { "arm64" } else { if arch() =~ "(x86|x86_64)" { "amd64" } else { "unsupported" } } +local_target := if os =~ "(darwin|linux|windows)" { os + "_" + arch } else { "unsupported" } +apps := "expert engine forge expert_credo" + +[doc('Run mix deps.get for the given project')] +deps project: + #!/usr/bin/env bash + cd apps/{{ project }} + mix deps.get + +[doc('Run an arbitrary command inside the given project directory')] +run project +ARGS: + #!/usr/bin/env bash + set -euo pipefail + cd apps/{{ project }} + eval "{{ ARGS }}" + +[doc('Compile the given project.')] +compile project: (deps project) + cd apps/{{ project }} && mix compile + +[doc('Run tests in the given project')] +test project="all" *args="": + #!/usr/bin/env bash + set -euo pipefail + + case {{ project }} in + all) + for proj in {{ apps }}; do + (cd "apps/$proj" && mix test {{args}}) + done + ;; + *) + (cd "apps/{{ project }}" && mix test {{args}}) + ;; + esac + +[doc('Run a mix command in one or all projects. Use `just test` to run tests.')] +mix cmd *project: + #!/usr/bin/env bash + + if [ -n "{{ project }}" ]; then + cd apps/{{ project }} + mix {{ cmd }} + else + for project in {{ apps }}; do + ( + cd apps/"$project" + + mix {{ cmd }} + ) + done + fi + +[doc('Lint all projects or just a single project')] +lint *project: + #!/usr/bin/env bash + set -euxo pipefail + + just mix "format --check-formatted" {{ project }} + just mix credo {{ project }} + just mix dialyzer {{ project }} + +build-engine: + #!/usr/bin/env bash + + cd apps/engine + mix build + + +[doc('Build a release for the local system')] +[unix] +release-local: (deps "expert") (compile "engine") build-engine + #!/usr/bin/env bash + cd apps/expert + + if [ "{{ local_target }}" == "unsupported" ]; then + echo "unsupported OS/Arch combination: {{ local_target }}" + exit 1 + fi + MIX_ENV=prod EXPERT_RELEASE_MODE=burrito BURRITO_TARGET="{{ local_target }}" mix release --overwrite + +[windows] +release-local: (deps "expert") (compile "engine") build-engine + # idk actually how to set env vars like this on windows, might crash + EXPERT_RELEASE_MODE=burrito BURRITO_TARGET="windows_amd64" MIX_ENV=prod mix release --no-compile + +[doc('Build releases for all target platforms')] +release-all: (compile "engine") + #!/usr/bin/env bash + cd apps/expert + EXPERT_RELEASE_MODE=burrito MIX_ENV=prod mix release --no-compile + +[doc('Build a plain release without burrito')] +release-plain: (compile "engine") + #!/usr/bin/env bash + cd apps/expert + MIX_ENV=prod mix release plain --overwrite + +default: release-local + diff --git a/mix.exs b/mix.exs deleted file mode 100644 index 22976d94..00000000 --- a/mix.exs +++ /dev/null @@ -1,58 +0,0 @@ -defmodule Expert.LanguageServer.MixProject do - use Mix.Project - - def project do - [ - apps_path: "apps", - version: "0.7.2", - start_permanent: Mix.env() == :prod, - deps: deps(), - aliases: aliases(), - docs: docs(), - name: "Expert", - consolidate_protocols: Mix.env() != :test - ] - end - - defp deps do - [ - {:ex_doc, "~> 0.34", only: :dev, runtime: false} - ] - end - - defp docs do - [ - main: "readme", - deps: [jason: "https://hexdocs.pm/jason/Jason.html"], - extras: ~w( - README.md - pages/installation.md - pages/architecture.md - pages/glossary.md - ), - groups_for_modules: [ - Core: ~r/(Expert|Engine)/, - Engine: ~r/Engine/, - Server: ~r/Expert/ - ] - ] - end - - defp aliases do - [ - compile: "compile --docs --debug-info", - docs: "docs --html", - test: "test --no-start", - "nix.hash": &nix_hash/1 - ] - end - - defp nix_hash(_args) do - docker = System.get_env("DOCKER_CMD", "docker") - - Mix.shell().cmd( - "#{docker} run --rm -v '#{File.cwd!()}:/data' nixos/nix nix --extra-experimental-features 'nix-command flakes' run ./data#update-hash", - stderr_to_stdout: false - ) - end -end diff --git a/mix.lock b/mix.lock deleted file mode 100644 index 8b754b59..00000000 --- a/mix.lock +++ /dev/null @@ -1,41 +0,0 @@ -%{ - "benchee": {:hex, :benchee, "1.3.1", "c786e6a76321121a44229dde3988fc772bca73ea75170a73fd5f4ddf1af95ccf", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "76224c58ea1d0391c8309a8ecbfe27d71062878f59bd41a390266bf4ac1cc56d"}, - "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, - "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, - "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, - "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "73ce7e0d239342fb9527d7ba567203e77dbb9b25", [ref: "73ce7e0d239342fb9527d7ba567203e77dbb9b25"]}, - "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, - "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, - "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, - "gen_lsp": {:hex, :gen_lsp, "0.11.0", "9eda4d2fcaff94d9b3062e322fcf524c176db1502f584a3cff6135088b46084b", [:mix], [{:jason, "~> 1.3", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:schematic, "~> 0.2.1", [hex: :schematic, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "d67c20650a5290a02f7bac53083ac4487d3c6b461f35a8b14c5d2d7638c20d26"}, - "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, - "logger_file_backend": {:hex, :logger_file_backend, "0.0.14", "774bb661f1c3fed51b624d2859180c01e386eb1273dc22de4f4a155ef749a602", [:mix], [], "hexpm", "071354a18196468f3904ef09413af20971d55164267427f6257b52cfba03f9e6"}, - "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, - "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, - "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, - "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, - "patch": {:hex, :patch, "0.15.0", "947dd6a8b24a2d2d1137721f20bb96a8feb4f83248e7b4ad88b4871d52807af5", [:mix], [], "hexpm", "e8dadf9b57b30e92f6b2b1ce2f7f57700d14c66d4ed56ee27777eb73fb77e58d"}, - "path_glob": {:hex, :path_glob, "0.2.0", "b9e34b5045cac5ecb76ef1aa55281a52bf603bf7009002085de40958064ca312", [:mix], [{:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "be2594cb4553169a1a189f95193d910115f64f15f0d689454bb4e8cfae2e7ebc"}, - "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"}, - "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.9", "4dc5e535832733df68df22f9de168b11c0c74bca65b27b088a10ac36dfb75d04", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1dccb04ec8544340e01608e108f32724458d0ac4b07e551406b3b920c40ba2e5"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, - "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, - "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"}, - "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, - "refactorex": {:hex, :refactorex, "0.1.51", "74fc4603b31b600d78539ffea9fe170038aa8d471eec5aed261354c9734b4b27", [:mix], [{:sourceror, "~> 1.7", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "aefa150ab2c0d62aa8c01c4d04b932806118790f09c4106e20883281932fba03"}, - "schematic": {:hex, :schematic, "0.2.1", "0b091df94146fd15a0a343d1bd179a6c5a58562527746dadd09477311698dbb1", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0b255d65921e38006138201cd4263fd8bb807d9dfc511074615cd264a571b3b1"}, - "snowflake": {:hex, :snowflake, "1.0.4", "8433b4e04fbed19272c55e1b7de0f7a1ee1230b3ae31a813b616fd6ef279e87a", [:mix], [], "hexpm", "badb07ebb089a5cff737738297513db3962760b10fe2b158ae3bebf0b4d5be13"}, - "sourceror": {:hex, :sourceror, "1.9.0", "3bf5fe2d017aaabe3866d8a6da097dd7c331e0d2d54e59e21c2b066d47f1e08e", [:mix], [], "hexpm", "d20a9dd5efe162f0d75a307146faa2e17b823ea4f134f662358d70f0332fed82"}, - "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, - "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"}, - "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, - "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, - "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, -}