fix: proper mac bundling with code signing
This commit is contained in:
@@ -23,7 +23,9 @@
|
||||
"WebFetch(domain:github.com)",
|
||||
"WebFetch(domain:raw.githubusercontent.com)",
|
||||
"Bash(echo \"EXIT: $?\")",
|
||||
"Bash(echo \"exit: $?\")"
|
||||
"Bash(echo \"exit: $?\")",
|
||||
"Bash(MIX_ENV=prod mix release bds --overwrite)",
|
||||
"Bash(MIX_ENV=prod mix bds.bundle.macos)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,6 +579,13 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-message-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.status-bar-item.theme-badge {
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 3px;
|
||||
|
||||
@@ -568,7 +568,7 @@
|
||||
</div>
|
||||
|
||||
<footer class="status-bar flex h-[22px] shrink-0 items-center justify-between gap-2" data-region="status-bar" data-testid="status-bar">
|
||||
<div class="status-bar-left flex min-w-0 items-center gap-2 overflow-hidden">
|
||||
<div class="status-bar-left flex min-w-0 items-center gap-2">
|
||||
<%= if @is_mac_ui do %>
|
||||
<div class="status-shell-controls flex items-center gap-1" data-testid="status-shell-controls">
|
||||
<button
|
||||
@@ -659,7 +659,7 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<button class="status-bar-item status-bar-task-button inline-flex items-center gap-2" data-testid="status-task-button" type="button" phx-click="open_tasks_panel">
|
||||
<button class="status-bar-item status-bar-task-button inline-flex min-w-0 items-center gap-2" data-testid="status-task-button" type="button" phx-click="open_tasks_panel">
|
||||
<%= if @status.left.running_task_message do %>
|
||||
<span class="task-spinner"></span>
|
||||
<% end %>
|
||||
|
||||
@@ -156,18 +156,81 @@ defmodule BDS.MacBundle do
|
||||
if Keyword.get(opts, :skip_codesign, false), do: :ok, else: codesign(app)
|
||||
end
|
||||
|
||||
# Mach-O magic numbers (thin 32/64-bit, both endiannesses, and fat) as the
|
||||
# first four bytes appear on disk.
|
||||
@macho_magics [
|
||||
<<0xCF, 0xFA, 0xED, 0xFE>>,
|
||||
<<0xCE, 0xFA, 0xED, 0xFE>>,
|
||||
<<0xFE, 0xED, 0xFA, 0xCF>>,
|
||||
<<0xFE, 0xED, 0xFA, 0xCE>>,
|
||||
<<0xCA, 0xFE, 0xBA, 0xBE>>,
|
||||
<<0xBE, 0xBA, 0xFE, 0xCA>>
|
||||
]
|
||||
|
||||
@doc """
|
||||
Ad-hoc codesign the whole bundle recursively. `--deep` signs every nested
|
||||
Mach-O (the relocated dylibs and the release's `beam.smp`/`erlexec`/escripts),
|
||||
then we verify with `--deep --strict`.
|
||||
Ad-hoc codesign the bundle.
|
||||
|
||||
`codesign --deep` only recurses into nested *bundles* and the main executable;
|
||||
it ignores the loose Mach-O files the release ships — `beam.smp`, `erlexec`,
|
||||
and every NIF `.so`/`.dylib` under `Resources/rel/…`. Those keep their original
|
||||
*linker-signed* ad-hoc signatures, which the macOS code-signing monitor rejects
|
||||
once the files are copied into a new bundle (the process is SIGKILLed with
|
||||
"Code Signature Invalid" the moment it `dlopen`s such a NIF).
|
||||
|
||||
So we sign every Mach-O explicitly with a fresh ad-hoc signature (inside-out),
|
||||
then seal the outer bundle and verify it.
|
||||
"""
|
||||
@spec codesign(String.t()) :: :ok | {:error, term()}
|
||||
def codesign(app) do
|
||||
with :ok <- run("codesign", ["--force", "--deep", "--sign", "-", "--timestamp=none", app]) do
|
||||
run("codesign", ["--verify", "--deep", "--strict", app])
|
||||
with :ok <- sign_machos(macho_files(app)),
|
||||
:ok <- run("codesign", ["--force", "--sign", "-", "--timestamp=none", app]) do
|
||||
run("codesign", ["--verify", "--strict", app])
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
List every Mach-O file under `root`, detected by magic number. These are the
|
||||
loose binaries `codesign --deep` skips and that must be signed individually.
|
||||
"""
|
||||
@spec macho_files(String.t()) :: [String.t()]
|
||||
def macho_files(root) do
|
||||
root |> regular_files() |> Enum.filter(&macho?/1)
|
||||
end
|
||||
|
||||
defp regular_files(path) do
|
||||
case File.ls(path) do
|
||||
{:ok, entries} ->
|
||||
Enum.flat_map(entries, fn entry ->
|
||||
full = Path.join(path, entry)
|
||||
|
||||
case File.lstat(full) do
|
||||
{:ok, %File.Stat{type: :directory}} -> regular_files(full)
|
||||
{:ok, %File.Stat{type: :regular}} -> [full]
|
||||
_ -> []
|
||||
end
|
||||
end)
|
||||
|
||||
{:error, _} ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp macho?(file) do
|
||||
case File.open(file, [:read, :binary], &IO.binread(&1, 4)) do
|
||||
{:ok, magic} when magic in @macho_magics -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp sign_machos(files) do
|
||||
Enum.reduce_while(files, :ok, fn file, :ok ->
|
||||
case run("codesign", ["--force", "--sign", "-", "--timestamp=none", file]) do
|
||||
:ok -> {:cont, :ok}
|
||||
error -> {:halt, error}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp run(cmd, args) do
|
||||
case System.cmd(cmd, args, stderr_to_stdout: true) do
|
||||
{_out, 0} -> :ok
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -174,6 +174,31 @@ defmodule BDS.MacBundleTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "MacBundle.macho_files/1" do
|
||||
test "finds Mach-O binaries anywhere in the tree by magic number, ignoring other files" do
|
||||
tmp = Path.join(System.tmp_dir!(), "bds-macho-#{System.unique_integer([:positive])}")
|
||||
on_exit(fn -> File.rm_rf!(tmp) end)
|
||||
|
||||
# Little-endian 64-bit Mach-O magic (0xFEEDFACF) as stored on disk.
|
||||
macho = <<0xCF, 0xFA, 0xED, 0xFE>>
|
||||
|
||||
beam = Path.join(tmp, "Contents/Resources/rel/erts-16/bin/beam.smp")
|
||||
nif = Path.join(tmp, "Contents/Resources/rel/lib/foo-1.0/priv/foo.so")
|
||||
dylib = Path.join(tmp, "Contents/Frameworks/libbar.dylib")
|
||||
|
||||
for path <- [beam, nif, dylib] do
|
||||
File.mkdir_p!(Path.dirname(path))
|
||||
File.write!(path, macho <> "machine code")
|
||||
end
|
||||
|
||||
# Non-Mach-O files (plist, shell scripts) must be excluded.
|
||||
File.write!(Path.join(tmp, "Contents/Info.plist"), ~s(<?xml version="1.0"?>))
|
||||
File.write!(Path.join(tmp, "Contents/Resources/rel/erts-16/bin/erl"), "#!/bin/sh\n")
|
||||
|
||||
assert Enum.sort(MacBundle.macho_files(tmp)) == Enum.sort([beam, nif, dylib])
|
||||
end
|
||||
end
|
||||
|
||||
describe "MacBundle.assemble/1" do
|
||||
setup do
|
||||
tmp = Path.join(System.tmp_dir!(), "bds-macbundle-#{System.unique_integer([:positive])}")
|
||||
|
||||
Reference in New Issue
Block a user