Make priv generation into a compiler task (#2978)

This commit is contained in:
Jonatan Kłosko 2025-04-03 12:14:01 +02:00 committed by GitHub
parent 28bbca9b0a
commit 9535e9e536
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 83 additions and 57 deletions

View file

@ -33,17 +33,17 @@ defmodule LivebookWeb.Endpoint do
# To account for both cases, we configure Plug.Static :from as MFA
# and return the accessible priv/ location in both scenarios.
#
# The priv/ static files are generated by the livebook.gen_priv task
# before building the escript or the release. We gzip the static
# files in priv/, since we want to serve them gzipped, and we don't
# include the non-gzipped ones to minimize app size. Note that we
# still have a separate static/ directory with the CI-precompiled
# assets, which we keep in Git so that people can install escript
# from GitHub or run MIX_ENV=prod phx.server, without Node and NPM.
# Storing minified assets is already not ideal, but we definitely
# want to avoid storing the gzipped variants in Git. That's why we
# store the assets uncompressed and then generate priv/static with
# their compressed variants as part of the build process.
# The priv/ static files are generated by the compile.livebook_priv
# as part of compilation. We gzip the static files in priv/, since
# we want to serve them gzipped, and we don't include the non-gzipped
# ones to minimize app size. Note that we still have a separate
# static/ directory with the CI-precompiled assets, which we keep
# in Git so that people can install escript from GitHub or run
# MIX_ENV=prod phx.server, without Node and NPM. Storing minified
# assets is already not ideal, but we definitely want to avoid
# storing the gzipped variants in Git. That's why we store the
# assets uncompressed and then generate priv/static with their
# compressed variants at compile time.
if code_reloading? do
# In development, we use assets from tmp/static_dev, which are

View file

@ -0,0 +1,70 @@
defmodule Mix.Tasks.Compile.LivebookPriv do
@moduledoc false
use Mix.Task
@gzippable_exts ~w(.js .css .txt .text .html .json .svg .eot .ttf)
@impl true
def run(_args) do
app_path = Mix.Project.app_path()
manifest_path = Path.join(app_path, "compile.livebook_priv")
prev_mtime =
case File.read(manifest_path) do
{:ok, binary} -> :erlang.binary_to_term(binary)
{:error, _error} -> nil
end
mtime1 =
compress_and_copy(
"static",
Path.join(app_path, "priv/static"),
prev_mtime
)
mtime2 =
compress_and_copy(
"iframe/priv/static/iframe",
Path.join(app_path, "priv/iframe_static"),
prev_mtime
)
mtime = max(mtime1, mtime2)
File.write!(manifest_path, :erlang.term_to_binary(mtime))
:ok
end
defp compress_and_copy(source_dir, target_dir, prev_mtime) do
source_paths = Path.wildcard(Path.join(source_dir, "**/*"))
mtime = paths_mtime(source_paths)
changed? = prev_mtime == nil or mtime > prev_mtime
if changed? do
File.rm_rf!(target_dir)
for source_path <- source_paths, File.regular?(source_path) do
target_path = Path.join(target_dir, Path.relative_to(source_path, source_dir))
File.mkdir_p!(Path.dirname(target_path))
if Path.extname(source_path) in @gzippable_exts do
content = source_path |> File.read!() |> :zlib.gzip()
File.write!(target_path <> ".gz", content)
else
File.cp!(source_path, target_path)
end
end
Mix.shell().info("Generated #{target_dir} with compressed files from #{source_dir}")
end
mtime
end
defp paths_mtime(paths) do
paths
|> Enum.map(fn path -> File.stat!(path).mtime end)
|> Enum.max()
end
end

View file

@ -1,42 +0,0 @@
defmodule Mix.Tasks.Livebook.GenPriv do
@moduledoc false
use Mix.Task
@gzippable_exts ~w(.js .css .txt .text .html .json .svg .eot .ttf)
@impl true
def run([]) do
# Use absolute paths, instead of relying on the current mix project,
# so the task can be invoked by nerves_livebook.
app_path = Application.app_dir(:livebook)
project_dir = Path.expand("../../..", __DIR__)
compress_and_copy(Path.join(project_dir, "static"), Path.join(app_path, "priv/static"))
compress_and_copy(
Path.join(project_dir, "iframe/priv/static/iframe"),
Path.join(app_path, "priv/iframe_static")
)
end
defp compress_and_copy(source_dir, target_dir) do
File.rm_rf!(target_dir)
source_paths = Path.wildcard(Path.join(source_dir, "**/*"))
for source_path <- source_paths, File.regular?(source_path) do
target_path = Path.join(target_dir, Path.relative_to(source_path, source_dir))
File.mkdir_p!(Path.dirname(target_path))
if Path.extname(source_path) in @gzippable_exts do
content = source_path |> File.read!() |> :zlib.gzip()
File.write!(target_path <> ".gz", content)
else
File.cp!(source_path, target_path)
end
end
Mix.shell().info("Generated #{target_dir} with compressed files from #{source_dir}")
end
end

View file

@ -18,6 +18,7 @@ defmodule Livebook.MixProject do
description: @description,
elixirc_paths: elixirc_paths(Mix.env()),
test_elixirc_options: [docs: true],
compilers: Mix.compilers() ++ [:livebook_priv],
start_permanent: Mix.env() == :prod,
listeners: [Phoenix.CodeReloader],
aliases: aliases(),
@ -69,10 +70,7 @@ defmodule Livebook.MixProject do
setup: ["deps.get", "cmd --cd assets npm install"],
"assets.deploy": ["cmd npm run deploy --prefix assets"],
"format.all": ["format", "cmd --cd assets npm run --silent format"],
"protobuf.generate": ["cmd --cd proto mix protobuf.generate"],
"phx.server": ["livebook.gen_priv", "phx.server"],
"escript.build": ["livebook.gen_priv", "escript.build"],
release: ["livebook.gen_priv", "release"]
"protobuf.generate": ["cmd --cd proto mix protobuf.generate"]
]
end