Skip to content

Commit ab7472e

Browse files
committed
feat: on the fly engine compilation
1 parent 9345e31 commit ab7472e

File tree

8 files changed

+187
-45
lines changed

8 files changed

+187
-45
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/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: 73 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,79 @@ 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+
args: [
187+
elixir,
188+
build_engine_script,
189+
"--source-path",
190+
engine_source,
191+
"--vsn",
192+
Expert.vsn()
193+
],
194+
cd: engine_source
195+
]
196+
197+
launcher = Expert.Port.path()
198+
199+
Logger.info("Finding or building engine for project #{Project.name(project)}")
200+
201+
port =
202+
Port.open(
203+
{:spawn_executable, launcher},
204+
opts
205+
)
206+
207+
wait_for_engine(port)
208+
end
209+
210+
defp wait_for_engine(port) do
211+
receive do
212+
{^port, {:data, ~c"engine_path:" ++ engine_path}} ->
213+
engine_path = engine_path |> to_string() |> String.trim()
214+
Logger.info("Engine build available at: #{engine_path}")
215+
216+
{:ok, ebin_paths(engine_path)}
217+
218+
{^port, _data} ->
219+
wait_for_engine(port)
220+
221+
{:EXIT, ^port, reason} ->
222+
Logger.error("Engine build script exited with reason: #{inspect(reason)}")
223+
{:error, reason}
224+
end
225+
end
226+
227+
defp ebin_paths(base_path) do
228+
base_path
171229
|> Path.join("lib/**/ebin")
172230
|> Path.wildcard()
173231
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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
major = :otp_release |> :erlang.system_info() |> List.to_string()
17+
version_file = Path.join([:code.root_dir(), "releases", major, "OTP_VERSION"])
18+
19+
erlang_vsn =
20+
try do
21+
{:ok, contents} = File.read(version_file)
22+
String.split(contents, "\n", trim: true)
23+
else
24+
[full] -> full
25+
_ -> major
26+
catch
27+
:error ->
28+
major
29+
end
30+
31+
elixir_vsn = System.version()
32+
33+
user_data_path = :filename.basedir(:user_data, "Expert", %{version: expert_vsn})
34+
out_path = Path.join([user_data_path, "engine-#{elixir_vsn}-otp-#{erlang_vsn}"])
35+
build_path = Path.join(out_path, "dev")
36+
37+
if not File.exists?(build_path) do
38+
System.put_env("MIX_INSTALL_DIR", out_path)
39+
40+
Mix.Task.run("local.hex", ["--force"])
41+
Mix.Task.run("local.rebar", ["--force"])
42+
Mix.Project.in_project(:engine, engine_source_path, [build_path: out_path], fn _module ->
43+
Mix.Task.run("compile", [])
44+
Mix.Task.run("namespace", [build_path, "--cwd", out_path])
45+
end)
46+
end
47+
48+
IO.puts("engine_path:" <> 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: 6 additions & 21 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,18 +64,18 @@ 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
90-
EXPERT_RELEASE_MODE=burrito MIX_ENV={{ env('MIX_ENV', 'prod')}} mix release --no-compile
75+
EXPERT_RELEASE_MODE=burrito MIX_ENV={{ env('MIX_ENV', 'prod')}} mix release --overwrite
9176

9277
[doc('Build a plain release without burrito')]
93-
release-plain: (compile "engine")
78+
release-plain: (deps "engine") (deps "expert")
9479
#!/usr/bin/env bash
9580
cd apps/expert
9681
MIX_ENV={{ env('MIX_ENV', 'prod')}} mix release plain --overwrite

0 commit comments

Comments
 (0)