Skip to content

Commit 51eb6f1

Browse files
authored
feat: on the fly engine builds (#24)
* feat: on the fly engine compilation * chore: use Mix.install instead of mix compile Also don't mark engine dependencies as optional, otherwise Mix.install won't compile them and the Engine will fail to start
1 parent 04cd32c commit 51eb6f1

File tree

9 files changed

+181
-47
lines changed

9 files changed

+181
-47
lines changed

apps/engine/lib/engine/completion.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
defmodule Engine.Completion do
22
alias Forge.Ast.Analysis
33
alias Forge.Ast.Env
4+
alias Forge.Completion.Candidate
45
alias Forge.Document
56
alias Forge.Document.Position
67

78
alias Engine.CodeMod.Format
8-
alias Forge.Completion.Candidate
99

1010
import Document.Line
1111
import Forge.Logging

apps/engine/mix.exs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ defmodule Engine.MixProject do
5151
github: "elixir-lsp/elixir_sense", ref: "e3ddc403554050221a2fd19a10a896fa7525bc02"},
5252
{:forge, path: "../forge"},
5353
{:gen_lsp, "~> 0.11"},
54-
{:patch, "~> 0.15", only: [:dev, :test], optional: true, runtime: false},
55-
{:path_glob, "~> 0.2", optional: true},
56-
{:phoenix_live_view, "~> 1.0", only: [:test], optional: true, runtime: false},
54+
{:patch, "~> 0.15", only: [:dev, :test], runtime: false},
55+
{:path_glob, "~> 0.2"},
56+
{:phoenix_live_view, "~> 1.0", only: [:test], runtime: false},
5757
{:sourceror, "~> 1.9"},
5858
{:stream_data, "~> 1.1", only: [:test], runtime: false},
5959
{:refactorex, "~> 0.1.52"}

apps/expert/lib/expert.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ defmodule Expert do
2323

2424
@dialyzer {:nowarn_function, apply_to_state: 2}
2525

26+
@version Mix.Project.config()[:version]
27+
28+
def vsn, do: @version
29+
2630
def get_lsp, do: :persistent_term.get(:expert_lsp, nil)
2731

2832
def start_link(args) do

apps/expert/lib/expert/engine_node.ex

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ defmodule Expert.EngineNode do
115115
bootstrap_args = [project, Document.Store.entropy(), all_app_configs()]
116116

117117
with {:ok, node_pid} <- EngineSupervisor.start_project_node(project),
118-
:ok <- start_node(project, glob_paths()),
118+
{:ok, glob_paths} <- glob_paths(project),
119+
:ok <- start_node(project, glob_paths),
119120
:ok <- :rpc.call(node_name, Engine.Bootstrap, :init, bootstrap_args),
120121
:ok <- ensure_apps_started(node_name) do
121122
{:ok, node_name, node_pid}
@@ -152,22 +153,80 @@ defmodule Expert.EngineNode do
152153
["/**/priv" | app_globs]
153154
end
154155

155-
def glob_paths do
156-
for entry <- :code.get_path(),
157-
entry_string = List.to_string(entry),
158-
entry_string != ".",
159-
Enum.any?(app_globs(), &PathGlob.match?(entry_string, &1, match_dot: true)) do
160-
entry
161-
end
156+
def glob_paths(_) do
157+
entries =
158+
for entry <- :code.get_path(),
159+
entry_string = List.to_string(entry),
160+
entry_string != ".",
161+
Enum.any?(app_globs(), &PathGlob.match?(entry_string, &1, match_dot: true)) do
162+
entry
163+
end
164+
165+
{:ok, entries}
162166
end
163167
else
164-
# In dev and prod environments, a default build of Engine is built
165-
# separately and copied to expert's priv directory.
166-
# When Engine is built in CI for a version matrix, we'll need to check if
167-
# we have the right version downloaded, and if not, we should download it.
168-
defp glob_paths do
169-
:expert
170-
|> :code.priv_dir()
168+
# In dev and prod environments, the engine source code is included in the
169+
# Expert release, and we build it on the fly for the project elixir+opt
170+
# versions if it was not built yet.
171+
defp glob_paths(%Project{} = project) do
172+
{:ok, elixir, _} = Expert.Port.elixir_executable(project)
173+
174+
expert_priv = :code.priv_dir(:expert)
175+
packaged_engine_source = Path.join([expert_priv, "engine_source", "apps", "engine"])
176+
177+
engine_source =
178+
"EXPERT_ENGINE_PATH"
179+
|> System.get_env(packaged_engine_source)
180+
|> Path.expand()
181+
182+
build_engine_script = Path.join(expert_priv, "build_engine.exs")
183+
184+
opts =
185+
[
186+
:stderr_to_stdout,
187+
args: [
188+
elixir,
189+
build_engine_script,
190+
"--source-path",
191+
engine_source,
192+
"--vsn",
193+
Expert.vsn()
194+
],
195+
cd: engine_source
196+
]
197+
198+
launcher = Expert.Port.path()
199+
200+
Logger.info("Finding or building engine for project #{Project.name(project)}")
201+
202+
port =
203+
Port.open(
204+
{:spawn_executable, launcher},
205+
opts
206+
)
207+
208+
wait_for_engine(port)
209+
end
210+
211+
defp wait_for_engine(port) do
212+
receive do
213+
{^port, {:data, ~c"engine_path:" ++ engine_path}} ->
214+
engine_path = engine_path |> to_string() |> String.trim()
215+
Logger.info("Engine build available at: #{engine_path}")
216+
217+
{:ok, ebin_paths(engine_path)}
218+
219+
{^port, _data} ->
220+
wait_for_engine(port)
221+
222+
{:EXIT, ^port, reason} ->
223+
Logger.error("Engine build script exited with reason: #{inspect(reason)}")
224+
{:error, reason}
225+
end
226+
end
227+
228+
defp ebin_paths(base_path) do
229+
base_path
171230
|> Path.join("lib/**/ebin")
172231
|> Path.wildcard()
173232
end

apps/expert/lib/expert/port.ex

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,6 @@ defmodule Expert.Port do
148148
end
149149

150150
def path({:unix, _}) do
151-
require Logger
152-
153151
with :non_existing <- :code.where_is_file(~c"port_wrapper.sh") do
154152
:expert
155153
|> :code.priv_dir()

apps/expert/lib/expert/release.ex

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,59 @@ defmodule Expert.Release do
22
def assemble(release) do
33
Mix.Task.run(:namespace, [release.path])
44

5-
engine_path = Path.expand("../../../engine", __DIR__)
5+
expert_root = Path.expand("../../../..", __DIR__)
6+
engine_path = Path.join([expert_root, "apps", "engine"])
7+
forge_path = Path.join([expert_root, "apps", "forge"])
68

7-
source = Path.join([engine_path, "_build/dev_ns"])
9+
engine_sources =
10+
[
11+
"lib",
12+
"deps",
13+
"mix.exs",
14+
"config",
15+
"mix.lock"
16+
]
17+
|> Enum.map(&Path.join([engine_path, &1]))
18+
19+
forge_sources =
20+
[
21+
"lib",
22+
"mix.exs",
23+
"config",
24+
"mix.lock"
25+
]
26+
|> Enum.map(&Path.join([forge_path, &1]))
27+
28+
root_exs = Path.join([expert_root, "*.exs"])
29+
version_file = Path.join([expert_root, "version.txt"])
830

931
dest =
1032
Path.join([
1133
release.path,
1234
"lib",
1335
"xp_expert-#{release.version}",
14-
"priv"
36+
"priv",
37+
"engine_source"
1538
])
1639

17-
File.cp_r!(source, dest)
40+
for source <- engine_sources do
41+
dest_path = Path.join([dest, "apps", "engine", Path.basename(source)])
42+
File.mkdir_p!(Path.dirname(dest_path))
43+
File.cp_r!(source, dest_path)
44+
end
45+
46+
for source <- forge_sources do
47+
dest_path = Path.join([dest, "apps", "forge", Path.basename(source)])
48+
File.mkdir_p!(Path.dirname(dest_path))
49+
File.cp_r!(source, dest_path)
50+
end
51+
52+
for exs_file <- Path.wildcard(root_exs) do
53+
dest_path = Path.join([dest, Path.basename(exs_file)])
54+
File.cp_r!(exs_file, dest_path)
55+
end
56+
57+
File.cp!(version_file, Path.join([dest, "version.txt"]))
1858

1959
release
2060
end

apps/expert/priv/build_engine.exs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{:ok, _} = Application.ensure_all_started(:elixir)
2+
{:ok, _} = Application.ensure_all_started(:mix)
3+
4+
{args, _, _} =
5+
OptionParser.parse(
6+
System.argv(),
7+
strict: [
8+
vsn: :string,
9+
source_path: :string
10+
]
11+
)
12+
13+
expert_vsn = Keyword.fetch!(args, :vsn)
14+
engine_source_path = Keyword.fetch!(args, :source_path)
15+
16+
expert_data_path = :filename.basedir(:user_data, "Expert", %{version: expert_vsn})
17+
18+
System.put_env("MIX_INSTALL_DIR", expert_data_path)
19+
20+
Mix.Task.run("local.hex", ["--force"])
21+
Mix.Task.run("local.rebar", ["--force"])
22+
23+
Mix.install([{:engine, path: engine_source_path, env: :dev}],
24+
start_applications: false,
25+
config_path: Path.join(engine_source_path, "config/config.exs"),
26+
lockfile: Path.join(engine_source_path, "mix.lock")
27+
)
28+
29+
install_path = Mix.install_project_dir()
30+
31+
dev_build_path = Path.join([install_path, "_build", "dev"])
32+
ns_build_path = Path.join([install_path, "_build", "dev_ns"])
33+
34+
File.rm_rf!(ns_build_path)
35+
File.cp_r!(dev_build_path, ns_build_path)
36+
37+
Mix.Task.run("namespace", [ns_build_path, "--cwd", install_path])
38+
39+
IO.puts("engine_path:" <> ns_build_path)

apps/forge/lib/mix/tasks/namespace.ex

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,16 @@ defmodule Mix.Tasks.Namespace do
3030

3131
require Logger
3232

33-
def run([base_directory]) do
33+
def run([base_directory | opts]) do
34+
{args, _, _} =
35+
OptionParser.parse(opts,
36+
strict: [cwd: :string]
37+
)
38+
39+
cwd = Keyword.get(args, :cwd, File.cwd!())
40+
41+
:persistent_term.put(:forge_namespace_cwd, cwd)
42+
3443
# Ensure we cache the loaded apps at the time of namespacing
3544
# Otherwise only the @extra_apps will be cached
3645
init()
@@ -124,7 +133,7 @@ defmodule Mix.Tasks.Namespace do
124133
end
125134

126135
defp discover_deps_apps do
127-
cwd = File.cwd!()
136+
cwd = :persistent_term.get(:forge_namespace_cwd, File.cwd!())
128137

129138
:application.loaded_applications()
130139
|> Enum.flat_map(fn {app_name, _description, _version} ->

justfile

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,24 +49,9 @@ lint *project="all":
4949
just mix {{ project }} credo
5050
just mix {{ project }} dialyzer
5151

52-
build-engine:
53-
#!/usr/bin/env bash
54-
set -euxo pipefail
55-
56-
cd apps/engine
57-
MIX_ENV=dev mix compile
58-
namespaced_dir=_build/dev_ns/
59-
rm -rf $namespaced_dir
60-
mkdir -p $namespaced_dir
61-
62-
cp -a _build/dev/. "$namespaced_dir"
63-
64-
MIX_ENV=dev mix namespace "$namespaced_dir"
65-
66-
6752
[doc('Build a release for the local system')]
6853
[unix]
69-
release-local: (deps "expert") (compile "engine") build-engine
54+
release-local: (deps "engine") (deps "expert")
7055
#!/usr/bin/env bash
7156
cd apps/expert
7257

@@ -79,12 +64,12 @@ release-local: (deps "expert") (compile "engine") build-engine
7964
MIX_ENV={{ env('MIX_ENV', 'prod')}} EXPERT_RELEASE_MODE=burrito BURRITO_TARGET="{{ local_target }}" mix release --overwrite
8065

8166
[windows]
82-
release-local: (deps "expert") (compile "engine") build-engine
67+
release-local: (deps "engine") (deps "expert")
8368
# idk actually how to set env vars like this on windows, might crash
84-
EXPERT_RELEASE_MODE=burrito BURRITO_TARGET="windows_amd64" MIX_ENV={{ env('MIX_ENV', 'prod')}} mix release --no-compile
69+
EXPERT_RELEASE_MODE=burrito BURRITO_TARGET="windows_amd64" MIX_ENV={{ env('MIX_ENV', 'prod')}} mix release --overwrite
8570

8671
[doc('Build releases for all target platforms')]
87-
release-all: (deps "expert") (compile "engine") build-engine
72+
release-all: (deps "engine") (deps "expert")
8873
#!/usr/bin/env bash
8974
cd apps/expert
9075

@@ -93,7 +78,7 @@ release-all: (deps "expert") (compile "engine") build-engine
9378
EXPERT_RELEASE_MODE=burrito MIX_ENV={{ env('MIX_ENV', 'prod')}} mix release --overwrite
9479

9580
[doc('Build a plain release without burrito')]
96-
release-plain: (compile "engine")
81+
release-plain: (deps "engine") (deps "expert")
9782
#!/usr/bin/env bash
9883
cd apps/expert
9984
MIX_ENV={{ env('MIX_ENV', 'prod')}} mix release plain --overwrite

0 commit comments

Comments
 (0)