mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-04 03:54:24 +08:00
App bundling improvements (#1211)
1. Replace `mix release mac_app|mac_app_dmg|windows_installer` with a single `mix release app` 2. Extract templates (Launcher.swift, Launcher.vbs, etc) into separate files in app_builder/lib/templates 3. Don't verify vc_redist.x64.exe checksum as the Microsoft publishes new releases on the same URL
This commit is contained in:
parent
79c453c8ee
commit
fc7328703a
17 changed files with 555 additions and 579 deletions
6
.github/scripts/app/bootstrap_mac.sh
vendored
6
.github/scripts/app/bootstrap_mac.sh
vendored
|
@ -17,7 +17,7 @@ main() {
|
|||
fi
|
||||
|
||||
export PATH=$PWD/tmp/wxwidgets-${wxwidgets_vsn}-$target/bin:$PATH
|
||||
echo "wx"
|
||||
echo "checking wx"
|
||||
file `which wxrc`
|
||||
wx-config --version
|
||||
echo
|
||||
|
@ -29,7 +29,7 @@ main() {
|
|||
fi
|
||||
|
||||
export PATH=$PWD/tmp/otp-${otp_vsn}-$target/bin:$PATH
|
||||
echo "otp"
|
||||
echo "checking otp"
|
||||
file `which erlc`
|
||||
erl +V
|
||||
erl -noshell -eval 'ok = crypto:start(), io:format("crypto ok~n"), halt().'
|
||||
|
@ -41,7 +41,7 @@ main() {
|
|||
fi
|
||||
|
||||
export PATH=$PWD/tmp/elixir-${elixir_vsn}/bin:$PATH
|
||||
echo "elixir"
|
||||
echo "checking elixir"
|
||||
elixir --version
|
||||
|
||||
cat << EOF > tmp/bootstrap_env.sh
|
||||
|
|
13
.github/scripts/app/build_mac.sh
vendored
13
.github/scripts/app/build_mac.sh
vendored
|
@ -3,18 +3,11 @@
|
|||
# Usage:
|
||||
#
|
||||
# $ sh .github/scripts/app/build_mac.sh
|
||||
# $ open _build/app_prod/rel/Livebook.app
|
||||
# $ open _build/app_prod/Livebook.app
|
||||
# $ open livebook://github.com/livebook-dev/livebook/blob/main/test/support/notebooks/basic.livemd
|
||||
# $ open ./test/support/notebooks/basic.livemd
|
||||
set -e
|
||||
|
||||
sh .github/scripts/app/bootstrap_mac.sh
|
||||
sh tmp/bootstrap_env.sh
|
||||
|
||||
# If CODESIGN_IDENITY is set, let's build the .dmg which would also notarize it.
|
||||
# Otherwise, let's build just the .app.
|
||||
if [ -n "$CODESIGN_IDENTITY" ]; then
|
||||
MIX_ENV=prod MIX_TARGET=app mix release mac_app_dmg --overwrite
|
||||
else
|
||||
MIX_ENV=prod MIX_TARGET=app mix release mac_app --overwrite
|
||||
fi
|
||||
. tmp/bootstrap_env.sh
|
||||
MIX_ENV=prod MIX_TARGET=app mix release app --overwrite
|
||||
|
|
4
.github/scripts/app/build_windows.sh
vendored
4
.github/scripts/app/build_windows.sh
vendored
|
@ -3,9 +3,9 @@
|
|||
# Usage:
|
||||
#
|
||||
# $ sh .github/scripts/app/build_windows.sh
|
||||
# $ _build/app_prod/rel/LivebookInstall.exe
|
||||
# $ wscript _build/app_prod/Livebook-win/LivebookLauncher.vbs
|
||||
# $ start livebook://github.com/livebook-dev/livebook/blob/main/test/support/notebooks/basic.livemd
|
||||
# $ start ./test/support/notebooks/basic.livemd
|
||||
set -e
|
||||
|
||||
MIX_ENV=prod MIX_TARGET=app mix release windows_installer --overwrite
|
||||
MIX_ENV=prod MIX_TARGET=app mix release app --overwrite
|
||||
|
|
1
app_builder/examples/wx_demo/example.wxdemo
Normal file
1
app_builder/examples/wx_demo/example.wxdemo
Normal file
|
@ -0,0 +1 @@
|
|||
An example file to test "open file" feature.
|
|
@ -24,7 +24,7 @@ defmodule WxDemo.Window do
|
|||
@wx_id_osx_hide 5250
|
||||
|
||||
def start_link(_) do
|
||||
{:wx_ref, _, _, pid} = :wx_object.start_link(__MODULE__, [], [])
|
||||
{:wx_ref, _, _, pid} = :wx_object.start_link({:local, __MODULE__}, __MODULE__, [], [])
|
||||
{:ok, pid}
|
||||
end
|
||||
|
||||
|
@ -47,11 +47,9 @@ defmodule WxDemo.Window do
|
|||
@impl true
|
||||
def init(_) do
|
||||
app_name = "WxDemo"
|
||||
|
||||
true = Process.register(self(), __MODULE__)
|
||||
os = os()
|
||||
wx = :wx.new()
|
||||
frame = :wxFrame.new(wx, -1, app_name, size: {100, 100})
|
||||
frame = :wxFrame.new(wx, -1, app_name, size: {400, 400})
|
||||
|
||||
if os == :macos do
|
||||
fixup_macos_menubar(frame, app_name)
|
||||
|
|
|
@ -26,51 +26,41 @@ defmodule WxDemo.MixProject do
|
|||
end
|
||||
|
||||
defp releases do
|
||||
options = [
|
||||
name: "WxDemo",
|
||||
url_schemes: ["wxdemo"],
|
||||
document_types: [
|
||||
%{
|
||||
name: "WxDemo",
|
||||
extensions: ["wxdemo"],
|
||||
# macos specific
|
||||
role: "Editor"
|
||||
}
|
||||
]
|
||||
]
|
||||
macos_notarization = macos_notarization()
|
||||
|
||||
[
|
||||
mac_app: [
|
||||
include_executables_for: [:unix],
|
||||
steps: [:assemble, &AppBuilder.build_mac_app(&1, options)]
|
||||
],
|
||||
mac_app_dmg: [
|
||||
include_executables_for: [:unix],
|
||||
steps: [:assemble, &build_mac_app_dmg(&1, options)]
|
||||
],
|
||||
windows_installer: [
|
||||
include_executables_for: [:windows],
|
||||
app: [
|
||||
steps: [
|
||||
:assemble,
|
||||
&AppBuilder.build_windows_installer(&1, [module: WxDemo.Window] ++ options)
|
||||
&AppBuilder.bundle/1
|
||||
],
|
||||
app: [
|
||||
name: "WxDemo",
|
||||
url_schemes: ["wxdemo"],
|
||||
document_types: [
|
||||
%{
|
||||
name: "WxDemo",
|
||||
extensions: ["wxdemo"],
|
||||
macos_role: "Editor"
|
||||
}
|
||||
],
|
||||
server: WxDemo,
|
||||
macos_build_dmg: macos_notarization != nil,
|
||||
macos_notarization: macos_notarization,
|
||||
windows_build_installer: true
|
||||
]
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
defp build_mac_app_dmg(release, options) do
|
||||
options =
|
||||
[
|
||||
codesign: [
|
||||
identity: System.fetch_env!("CODESIGN_IDENTITY")
|
||||
],
|
||||
notarize: [
|
||||
team_id: System.fetch_env!("NOTARIZE_TEAM_ID"),
|
||||
apple_id: System.fetch_env!("NOTARIZE_APPLE_ID"),
|
||||
password: System.fetch_env!("NOTARIZE_PASSWORD")
|
||||
]
|
||||
] ++ options
|
||||
defp macos_notarization do
|
||||
identity = System.get_env("NOTARIZE_IDENTITY")
|
||||
team_id = System.get_env("NOTARIZE_TEAM_ID")
|
||||
apple_id = System.get_env("NOTARIZE_APPLE_ID")
|
||||
password = System.get_env("NOTARIZE_PASSWORD")
|
||||
|
||||
AppBuilder.build_mac_app_dmg(release, options)
|
||||
if identity && team_id && apple_id && password do
|
||||
[identity: identity, team_id: team_id, apple_id: apple_id, password: password]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,9 +1,32 @@
|
|||
defmodule AppBuilder do
|
||||
defdelegate build_mac_app(release, options), to: AppBuilder.MacOS
|
||||
def bundle(release) do
|
||||
os = os()
|
||||
|
||||
defdelegate build_mac_app_dmg(release, options), to: AppBuilder.MacOS
|
||||
allowed_options = [
|
||||
:name,
|
||||
:server,
|
||||
icon_path: [
|
||||
macos: Application.app_dir(:wx, "examples/demo/erlang.png")
|
||||
],
|
||||
url_schemes: [],
|
||||
document_types: [],
|
||||
additional_paths: [],
|
||||
macos_is_agent_app: false,
|
||||
macos_build_dmg: false,
|
||||
macos_notarization: nil,
|
||||
windows_build_installer: true
|
||||
]
|
||||
|
||||
defdelegate build_windows_installer(release, options), to: AppBuilder.Windows
|
||||
options = Keyword.validate!(release.options[:app], allowed_options)
|
||||
|
||||
case os do
|
||||
:macos ->
|
||||
AppBuilder.MacOS.bundle(release, options)
|
||||
|
||||
:windows ->
|
||||
AppBuilder.Windows.bundle(release, options)
|
||||
end
|
||||
end
|
||||
|
||||
def os do
|
||||
case :os.type() do
|
||||
|
|
|
@ -3,74 +3,120 @@ defmodule AppBuilder.MacOS do
|
|||
|
||||
import AppBuilder.Utils
|
||||
|
||||
def build_mac_app_dmg(release, options) do
|
||||
{codesign, options} = Keyword.pop(options, :codesign)
|
||||
{notarize, options} = Keyword.pop(options, :notarize)
|
||||
@templates_path "#{__ENV__.file}/../../templates"
|
||||
|
||||
release = build_mac_app(release, options)
|
||||
def bundle(release, options) do
|
||||
app_name = options[:name]
|
||||
|
||||
app_path = "#{Mix.Project.build_path()}/#{app_name}.app"
|
||||
File.rm_rf!(app_path)
|
||||
tmp_dir = "#{Mix.Project.build_path()}/tmp"
|
||||
contents_path = "#{app_path}/Contents"
|
||||
resources_path = "#{contents_path}/Resources"
|
||||
|
||||
copy_dir(release.path, "#{resources_path}/rel")
|
||||
|
||||
launcher_eex_path = Path.expand("#{@templates_path}/macos/Launcher.swift.eex")
|
||||
launcher_src_path = "#{tmp_dir}/Launcher.swift"
|
||||
launcher_bin_path = "#{contents_path}/MacOS/#{app_name}Launcher"
|
||||
copy_template(launcher_eex_path, launcher_src_path, release: release, app_options: options)
|
||||
|
||||
File.mkdir!("#{contents_path}/MacOS")
|
||||
log(:green, :creating, Path.relative_to_cwd(launcher_bin_path))
|
||||
|
||||
cmd!("swiftc", [
|
||||
"-warnings-as-errors",
|
||||
"-target",
|
||||
swiftc_target(),
|
||||
"-o",
|
||||
launcher_bin_path,
|
||||
launcher_src_path
|
||||
])
|
||||
|
||||
icon_path = Keyword.fetch!(options, :icon_path)
|
||||
dest_path = "#{resources_path}/AppIcon.icns"
|
||||
create_icon(icon_path, dest_path)
|
||||
|
||||
for type <- Keyword.fetch!(options, :document_types) do
|
||||
if src_path = type[:icon_path] do
|
||||
dest_path = "#{resources_path}/#{type.name}Icon.icns"
|
||||
create_icon(src_path, dest_path)
|
||||
end
|
||||
end
|
||||
|
||||
copy_template(
|
||||
Path.expand("#{@templates_path}/macos/Info.plist.eex"),
|
||||
"#{contents_path}/Info.plist",
|
||||
release: release,
|
||||
app_options: options
|
||||
)
|
||||
|
||||
if options[:macos_build_dmg] do
|
||||
build_dmg(release, options)
|
||||
end
|
||||
|
||||
release
|
||||
end
|
||||
|
||||
defp build_dmg(release, options) do
|
||||
app_name = Keyword.fetch!(options, :name)
|
||||
File.rm_rf!("tmp/dmg")
|
||||
File.mkdir_p!("tmp/dmg")
|
||||
File.ln_s!("/Applications", "tmp/dmg/Applications")
|
||||
notarization = Keyword.fetch!(options, :macos_notarization)
|
||||
|
||||
dmg_dir = "#{Mix.Project.build_path()}/dmg"
|
||||
app_dir = "#{dmg_dir}/#{app_name}.app"
|
||||
tmp_dir = "#{Mix.Project.build_path()}/tmp"
|
||||
File.rm_rf!(dmg_dir)
|
||||
File.mkdir_p!(dmg_dir)
|
||||
|
||||
File.ln_s!("/Applications", "#{dmg_dir}/Applications")
|
||||
|
||||
File.cp_r!(
|
||||
Path.join([Mix.Project.build_path(), "rel", "#{app_name}.app"]),
|
||||
"tmp/dmg/#{app_name}.app"
|
||||
"#{Mix.Project.build_path()}/#{app_name}.app",
|
||||
app_dir
|
||||
)
|
||||
|
||||
to_sign =
|
||||
"tmp/dmg/#{app_name}.app/**"
|
||||
"#{app_dir}/**"
|
||||
|> Path.wildcard()
|
||||
|> Enum.filter(fn file ->
|
||||
stat = File.lstat!(file)
|
||||
Bitwise.band(0o100, stat.mode) != 0 and stat.type == :regular
|
||||
end)
|
||||
|
||||
to_sign = to_sign ++ ["tmp/dmg/#{app_name}.app"]
|
||||
to_sign = to_sign ++ [app_dir]
|
||||
|
||||
if codesign do
|
||||
entitlements_path = "tmp/entitlements.plist"
|
||||
File.write!(entitlements_path, entitlements())
|
||||
codesign(to_sign, "--options=runtime --entitlements=#{entitlements_path}", codesign)
|
||||
end
|
||||
entitlements_eex_path = "#{Path.expand(@templates_path)}/macos/Entitlements.plist.eex"
|
||||
entitlements_plist_path = "#{tmp_dir}/Entitlements.plist"
|
||||
|
||||
copy_template(entitlements_eex_path, entitlements_plist_path,
|
||||
release: release,
|
||||
app_options: options
|
||||
)
|
||||
|
||||
log(:green, "signing", Path.relative_to_cwd(app_dir))
|
||||
codesign(to_sign, "--options=runtime --entitlements=#{entitlements_plist_path}", notarization)
|
||||
|
||||
arch = :erlang.system_info(:system_architecture) |> to_string |> String.split("-") |> hd()
|
||||
vsn = release.version
|
||||
basename = "#{app_name}-#{vsn}-#{arch}.dmg"
|
||||
|
||||
tmp_dmg_path = "tmp/#{app_name}.dmg"
|
||||
dmg_path = "#{Mix.Project.build_path()}/rel/#{basename}"
|
||||
|
||||
File.rm_rf!(tmp_dmg_path)
|
||||
File.rm_rf!(dmg_path)
|
||||
dmg_path = "#{Mix.Project.build_path()}/#{app_name}Install-#{vsn}-#{arch}.dmg"
|
||||
log(:green, "creating", Path.relative_to_cwd(dmg_path))
|
||||
|
||||
cmd!(
|
||||
"hdiutil",
|
||||
~w(create #{tmp_dmg_path} -ov -volname #{app_name}Install -fs HFS+ -srcfolder tmp/dmg)
|
||||
~w(create #{dmg_path} -ov -volname #{app_name}Install -fs HFS+ -srcfolder #{dmg_dir})
|
||||
)
|
||||
|
||||
cmd!(
|
||||
"hdiutil",
|
||||
~w(convert #{tmp_dmg_path} -format UDZO -o #{dmg_path})
|
||||
)
|
||||
log(:green, "notarizing", Path.relative_to_cwd(dmg_path))
|
||||
notarize(dmg_path, notarization)
|
||||
|
||||
if codesign do
|
||||
codesign([dmg_path], "", codesign)
|
||||
end
|
||||
|
||||
if notarize do
|
||||
notarize(dmg_path, notarize)
|
||||
end
|
||||
|
||||
File.rm!(tmp_dmg_path)
|
||||
release
|
||||
end
|
||||
|
||||
defp codesign(paths, args, options) do
|
||||
defp codesign(paths, extra_flags, options) do
|
||||
identity = Keyword.fetch!(options, :identity)
|
||||
paths = Enum.join(paths, " ")
|
||||
shell!("codesign --force --timestamp --verbose=4 --sign=\"#{identity}\" #{args} #{paths}")
|
||||
flags = "--force --timestamp --verbose=4 --sign=\"#{identity}\" #{extra_flags}"
|
||||
shell!("codesign #{flags} #{paths}")
|
||||
end
|
||||
|
||||
defp notarize(path, options) do
|
||||
|
@ -89,106 +135,8 @@ defmodule AppBuilder.MacOS do
|
|||
""")
|
||||
end
|
||||
|
||||
def build_mac_app(release, options) do
|
||||
options =
|
||||
Keyword.validate!(options, [
|
||||
:name,
|
||||
:version,
|
||||
:icon_path,
|
||||
:info_plist,
|
||||
:url_schemes,
|
||||
:document_types,
|
||||
:additional_paths,
|
||||
:is_agent_app
|
||||
])
|
||||
|
||||
app_name = Keyword.fetch!(options, :name)
|
||||
additional_paths = Keyword.get(options, :additional_paths, [])
|
||||
|
||||
app_bundle_path = Path.join([Mix.Project.build_path(), "rel", "#{app_name}.app"])
|
||||
File.rm_rf!(app_bundle_path)
|
||||
File.mkdir_p!(Path.join([app_bundle_path, "Contents", "Resources"]))
|
||||
File.rename!(release.path, Path.join([app_bundle_path, "Contents", "Resources", "rel"]))
|
||||
|
||||
File.mkdir_p!("tmp")
|
||||
launcher_src_path = "tmp/Launcher.swift"
|
||||
File.write!(launcher_src_path, launcher(release, additional_paths))
|
||||
launcher_path = Path.join([app_bundle_path, "Contents", "MacOS", app_name <> "Launcher"])
|
||||
File.mkdir_p!(Path.dirname(launcher_path))
|
||||
|
||||
cmd!("swiftc", [
|
||||
"-warnings-as-errors",
|
||||
"-target",
|
||||
swiftc_target(),
|
||||
"-o",
|
||||
launcher_path,
|
||||
launcher_src_path
|
||||
])
|
||||
|
||||
icon_path = options[:icon_path] || Application.app_dir(:wx, "examples/demo/erlang.png")
|
||||
dest_path = Path.join([app_bundle_path, "Contents", "Resources", "AppIcon.icns"])
|
||||
create_icon(icon_path, dest_path)
|
||||
|
||||
for type <- options[:document_types] || [] do
|
||||
if src_path = type[:icon_path] do
|
||||
dest_path = Path.join([app_bundle_path, "Contents", "Resources", "#{type.name}Icon.icns"])
|
||||
create_icon(src_path, dest_path)
|
||||
end
|
||||
end
|
||||
|
||||
info_plist = options[:info_plist] || info_plist(options)
|
||||
File.write!(Path.join([app_bundle_path, "Contents", "Info.plist"]), info_plist)
|
||||
|
||||
release
|
||||
end
|
||||
|
||||
defp launcher(release, additional_paths) do
|
||||
additional_paths = Enum.map_join(additional_paths, ":", &"\\(resourcePath)#{&1}")
|
||||
|
||||
"""
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
let fm = FileManager.default
|
||||
let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as! String
|
||||
let home = NSHomeDirectory()
|
||||
|
||||
let logPath = "\\(home)/Library/Logs/\\(appName).log"
|
||||
if !fm.fileExists(atPath: logPath) { fm.createFile(atPath: logPath, contents: Data()) }
|
||||
let logFile = FileHandle(forUpdatingAtPath: logPath)
|
||||
logFile?.seekToEndOfFile()
|
||||
|
||||
let releaseScriptPath = Bundle.main.path(forResource: "rel/bin/#{release.name}", ofType: "")!
|
||||
|
||||
let resourcePath = Bundle.main.resourcePath ?? ""
|
||||
let additionalPaths = "#{additional_paths}"
|
||||
|
||||
var environment = ProcessInfo.processInfo.environment
|
||||
let path = environment["PATH"] ?? ""
|
||||
|
||||
environment["PATH"] = "\\(additionalPaths):\\(path)"
|
||||
|
||||
let task = Process()
|
||||
task.environment = environment
|
||||
task.launchPath = releaseScriptPath
|
||||
task.arguments = ["start"]
|
||||
task.standardOutput = logFile
|
||||
task.standardError = logFile
|
||||
try task.run()
|
||||
|
||||
task.waitUntilExit()
|
||||
|
||||
if task.terminationStatus != 0 {
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .critical
|
||||
alert.messageText = "\\(appName) exited with error status \\(task.terminationStatus)."
|
||||
alert.informativeText = "Logs available at \\(logPath)."
|
||||
alert.runModal()
|
||||
}
|
||||
"""
|
||||
end
|
||||
|
||||
defp create_icon(src_path, dest_path) do
|
||||
log(:green, :creating, Path.relative_to_cwd(dest_path))
|
||||
src_path = normalize_icon_path(src_path)
|
||||
|
||||
if Path.extname(src_path) == ".icns" do
|
||||
|
@ -210,7 +158,7 @@ defmodule AppBuilder.MacOS do
|
|||
|
||||
size = size * scale
|
||||
out = "#{dest_tmp_path}/icon_#{size}x#{size}#{suffix}.png"
|
||||
cmd!("sips", ~w(-z #{size} #{size} #{src_path} --out #{out}))
|
||||
cmd!("sips", ~w(-z #{size} #{size} #{src_path} --out #{out}), into: "")
|
||||
end
|
||||
|
||||
cmd!("iconutil", ~w(-c icns #{dest_tmp_path} -o #{dest_path}))
|
||||
|
@ -227,101 +175,4 @@ defmodule AppBuilder.MacOS do
|
|||
"arm64-apple-macosx12"
|
||||
end
|
||||
end
|
||||
|
||||
## Templates
|
||||
|
||||
require EEx
|
||||
|
||||
defp entitlements do
|
||||
"""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
"""
|
||||
end
|
||||
|
||||
code = """
|
||||
<%
|
||||
app_name = Keyword.fetch!(options, :name)
|
||||
app_version = Keyword.fetch!(options, :version)
|
||||
%>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string><%= app_name %>Launcher</string>
|
||||
<key>CFBundleName</key>
|
||||
<string><%= app_name %></string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string><%= app_name %></string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string><%= app_version %></string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string><%= app_version %></string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
|
||||
<%= if schemes = options[:url_schemes] do %>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<%= for scheme <- schemes do %>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string><%= app_name %></string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string><%= scheme %></string>
|
||||
</array>
|
||||
</dict>
|
||||
<% end %>
|
||||
</array>
|
||||
<% end %>
|
||||
|
||||
<%= if types = options[:document_types] do %>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<%= for type <- types do %>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string><%= type.name %></string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string><%= type.role %></string>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<%= for ext <- type.extensions do %>
|
||||
<string><%= ext %></string>
|
||||
<% end %>
|
||||
</array>
|
||||
<%= if type[:icon_path] do %>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string><%= type.name %>Icon</string>
|
||||
<% end %>
|
||||
</dict>
|
||||
<% end %>
|
||||
</array>
|
||||
<% end %>
|
||||
|
||||
<%= if options[:is_agent_app] do %>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<% end %>
|
||||
</dict>
|
||||
</plist>
|
||||
"""
|
||||
|
||||
EEx.function_from_string(:defp, :info_plist, code, [:options], trim: true)
|
||||
end
|
||||
|
|
|
@ -5,7 +5,11 @@ defmodule AppBuilder.Utils do
|
|||
|
||||
def cmd!(bin, args, opts \\ []) do
|
||||
opts = Keyword.put_new(opts, :into, IO.stream())
|
||||
{_, 0} = System.cmd(bin, args, opts)
|
||||
{_, status} = System.cmd(bin, args, opts)
|
||||
|
||||
if status != 0 do
|
||||
raise "command exited with #{status}"
|
||||
end
|
||||
end
|
||||
|
||||
def shell!(command, opts \\ []) do
|
||||
|
@ -13,11 +17,17 @@ defmodule AppBuilder.Utils do
|
|||
{_, 0} = System.shell(command, opts)
|
||||
end
|
||||
|
||||
def ensure_executable(url) do
|
||||
ensure_executable(url, :no_verify)
|
||||
end
|
||||
|
||||
def ensure_executable(url, expected_sha256) do
|
||||
tmp_dir = Path.join(System.tmp_dir!(), Path.basename(url, Path.extname(url)))
|
||||
path = Path.join(tmp_dir, Path.basename(url))
|
||||
|
||||
unless File.exists?(path) do
|
||||
if File.exists?(path) do
|
||||
verify(File.read!(path), expected_sha256)
|
||||
else
|
||||
File.mkdir_p!(tmp_dir)
|
||||
body = download_and_verify(url, expected_sha256)
|
||||
File.write!(path, body)
|
||||
|
@ -53,6 +63,10 @@ defmodule AppBuilder.Utils do
|
|||
body
|
||||
end
|
||||
|
||||
defp verify(data, :no_verify) do
|
||||
data
|
||||
end
|
||||
|
||||
defp verify(data, expected_sha256) do
|
||||
actual_sha256 = :crypto.hash(:sha256, data) |> Base.encode16(case: :lower)
|
||||
|
||||
|
@ -74,4 +88,30 @@ defmodule AppBuilder.Utils do
|
|||
def normalize_icon_path(path_per_os) when is_list(path_per_os) do
|
||||
Keyword.fetch!(path_per_os, AppBuilder.os())
|
||||
end
|
||||
|
||||
def copy_dir(from, to, options \\ []) do
|
||||
File.mkdir_p!(Path.dirname(to))
|
||||
log(:green, "creating", Path.relative_to_cwd(to), options)
|
||||
File.cp_r!(from, to)
|
||||
end
|
||||
|
||||
def copy_file(source, target, options \\ []) do
|
||||
create_file(target, File.read!(source), options)
|
||||
end
|
||||
|
||||
def copy_template(source, target, assigns, options \\ []) do
|
||||
create_file(target, EEx.eval_file(source, assigns: assigns, trim: true), options)
|
||||
end
|
||||
|
||||
def create_file(path, contents, options \\ []) when is_binary(path) do
|
||||
log(:green, :creating, Path.relative_to_cwd(path), options)
|
||||
File.mkdir_p!(Path.dirname(path))
|
||||
File.write!(path, contents)
|
||||
end
|
||||
|
||||
def log(color, command, message, options \\ []) do
|
||||
unless options[:quiet] do
|
||||
Mix.shell().info([color, "* #{command} ", :reset, message])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,52 @@ defmodule AppBuilder.Windows do
|
|||
@moduledoc false
|
||||
|
||||
import AppBuilder.Utils
|
||||
require EEx
|
||||
|
||||
@templates_path "#{__ENV__.file}/../../templates"
|
||||
|
||||
def bundle(release, options) do
|
||||
app_name = options[:name]
|
||||
|
||||
app_path = "#{Mix.Project.build_path()}/#{app_name}-win"
|
||||
File.rm_rf!(app_path)
|
||||
|
||||
copy_dir(release.path, "#{app_path}/rel")
|
||||
|
||||
launcher_eex_path = Path.expand("#{@templates_path}/windows/Launcher.vbs.eex")
|
||||
launcher_bin_path = "#{app_path}/#{app_name}Launcher.vbs"
|
||||
copy_template(launcher_eex_path, launcher_bin_path, release: release, app_options: options)
|
||||
File.mkdir!("#{app_path}/Logs")
|
||||
|
||||
manifest_eex_path = Path.expand("#{@templates_path}/windows/Manifest.xml.eex")
|
||||
manifest_xml_path = "#{app_path}/Manifest.xml"
|
||||
copy_template(manifest_eex_path, manifest_xml_path, release: release)
|
||||
|
||||
rcedit_path = ensure_rcedit()
|
||||
erl_exe = "#{app_path}/rel/erts-#{release.erts_version}/bin/erl.exe"
|
||||
log(:green, :updating, Path.relative_to_cwd(erl_exe))
|
||||
cmd!(rcedit_path, ["--application-manifest", manifest_xml_path, erl_exe])
|
||||
|
||||
vcredist_path = ensure_vcredistx64()
|
||||
copy_file(vcredist_path, "#{app_path}/vcredist_x64.exe")
|
||||
|
||||
create_icon(options[:icon_path], "#{app_path}/AppIcon.ico")
|
||||
|
||||
for type <- Keyword.fetch!(options, :document_types) do
|
||||
if src_path = type[:icon_path] do
|
||||
dest_path = "#{app_path}/#{type.name}Icon.ico"
|
||||
create_icon(src_path, dest_path)
|
||||
end
|
||||
end
|
||||
|
||||
installer_eex_path = Path.expand("#{@templates_path}/windows/Installer.nsi.eex")
|
||||
installer_nsi_path = "#{app_path}/Installer.nsi"
|
||||
copy_template(installer_eex_path, installer_nsi_path, release: release, app_options: options)
|
||||
makensis_path = ensure_makensis()
|
||||
log(:green, "creating", Path.relative_to_cwd("#{app_path}/#{app_name}Install.exe"))
|
||||
cmd!(makensis_path, [installer_nsi_path])
|
||||
|
||||
release
|
||||
end
|
||||
|
||||
def __send_events__(server, input)
|
||||
|
||||
|
@ -27,218 +72,9 @@ defmodule AppBuilder.Windows do
|
|||
send(server, {:open_file, path})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a Windows installer.
|
||||
"""
|
||||
def build_windows_installer(release, options) do
|
||||
tmp_dir = release.path <> "_tmp"
|
||||
File.rm_rf(tmp_dir)
|
||||
File.mkdir_p!(tmp_dir)
|
||||
|
||||
File.cp_r!(release.path, Path.join(tmp_dir, "rel"))
|
||||
|
||||
options =
|
||||
Keyword.validate!(options, [
|
||||
:name,
|
||||
:version,
|
||||
:url_schemes,
|
||||
:document_types,
|
||||
:icon_path,
|
||||
:module
|
||||
])
|
||||
|
||||
app_name = Keyword.fetch!(options, :name)
|
||||
|
||||
vcredist_path = ensure_vcredistx64()
|
||||
File.cp!(vcredist_path, Path.join(tmp_dir, "vcredist_x64.exe"))
|
||||
|
||||
icon_path = options[:icon_path] || Application.app_dir(:wx, "examples/demo/erlang.png")
|
||||
app_icon_path = Path.join(tmp_dir, "app_icon.ico")
|
||||
create_icon(icon_path, app_icon_path)
|
||||
|
||||
erl_exe = Path.join([tmp_dir, "rel", "erts-#{release.erts_version}", "bin", "erl.exe"])
|
||||
rcedit_path = ensure_rcedit()
|
||||
cmd!(rcedit_path, ["--set-icon", app_icon_path, erl_exe])
|
||||
manifest_path = Path.join(tmp_dir, "manifest.xml")
|
||||
File.write!(manifest_path, manifest())
|
||||
cmd!(rcedit_path, ["--application-manifest", manifest_path, erl_exe])
|
||||
|
||||
File.write!(Path.join(tmp_dir, "#{app_name}.vbs"), launcher_vbs(release, options))
|
||||
nsi_path = Path.join(tmp_dir, "#{app_name}.nsi")
|
||||
File.write!(nsi_path, nsi(options))
|
||||
makensis_path = ensure_makensis()
|
||||
cmd!(makensis_path, [nsi_path])
|
||||
|
||||
File.rename!(
|
||||
Path.join(tmp_dir, "#{app_name}Install.exe"),
|
||||
Path.join([Mix.Project.build_path(), "rel", "#{app_name}Install.exe"])
|
||||
)
|
||||
|
||||
release
|
||||
end
|
||||
|
||||
# https://docs.microsoft.com/en-us/windows/win32/hidpi/setting-the-default-dpi-awareness-for-a-process
|
||||
defp manifest do
|
||||
"""
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
"""
|
||||
end
|
||||
|
||||
code = """
|
||||
<%
|
||||
app_name = Keyword.fetch!(options, :name)
|
||||
url_schemes = Keyword.get(options, :url_schemes, [])
|
||||
%>
|
||||
!include "MUI2.nsh"
|
||||
|
||||
;--------------------------------
|
||||
;General
|
||||
|
||||
Name "<%= app_name %>"
|
||||
OutFile "<%= app_name %>Install.exe"
|
||||
Unicode True
|
||||
InstallDir "$LOCALAPPDATA\\<%= app_name %>"
|
||||
|
||||
; Need admin for registering URL scheme
|
||||
RequestExecutionLevel admin
|
||||
|
||||
;--------------------------------
|
||||
;Interface Settings
|
||||
|
||||
!define MUI_ABORTWARNING
|
||||
|
||||
;--------------------------------
|
||||
;Pages
|
||||
|
||||
;!insertmacro MUI_PAGE_COMPONENTS
|
||||
!define MUI_ICON "app_icon.ico"
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
|
||||
;--------------------------------
|
||||
;Languages
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
;--------------------------------
|
||||
;Installer Sections
|
||||
|
||||
Section "Install"
|
||||
SetOutPath "$INSTDIR"
|
||||
|
||||
File vcredist_x64.exe
|
||||
ExecWait '"$INSTDIR\\vcredist_x64.exe" /install'
|
||||
|
||||
File /r rel rel
|
||||
File "<%= app_name %>.vbs"
|
||||
File "app_icon.ico"
|
||||
|
||||
CreateDirectory "$INSTDIR\\Logs"
|
||||
WriteUninstaller "$INSTDIR\\<%= app_name %>Uninstall.exe"
|
||||
|
||||
<%= for type <- Keyword.get(options, :document_types, []) do %>
|
||||
<%= for ext <- type.extensions do %>
|
||||
WriteRegStr HKCR ".<%= ext %>" "" "<%= app_name %>.<%= type.name %>"
|
||||
<% end %>
|
||||
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>" "" "<%= type.name %>"
|
||||
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>\\DefaultIcon" "" "$INSTDIR\\app_icon.ico"
|
||||
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>\\shell\\open\\command" "" '$WINDIR\\system32\\wscript.exe "$INSTDIR\\<%= app_name %>.vbs" "open_file:%1"'
|
||||
<% end %>
|
||||
|
||||
<%= for url_scheme <- url_schemes do %>
|
||||
DetailPrint "Register <%= url_scheme %> URL Handler"
|
||||
DeleteRegKey HKCR "<%= url_scheme %>"
|
||||
WriteRegStr HKCR "<%= url_scheme %>" "" "<%= url_scheme %> Protocol"
|
||||
WriteRegStr HKCR "<%= url_scheme %>" "URL Protocol" ""
|
||||
WriteRegStr HKCR "<%= url_scheme %>\\shell" "" ""
|
||||
WriteRegStr HKCR "<%= url_scheme %>\\shell\\open" "" ""
|
||||
WriteRegStr HKCR "<%= url_scheme %>\\shell\\open\\command" "" '$WINDIR\\system32\\wscript.exe "$INSTDIR\\<%= app_name %>.vbs" "open_url:%1"'
|
||||
<% end %>
|
||||
SectionEnd
|
||||
|
||||
Section "Desktop Shortcut"
|
||||
CreateShortCut "$DESKTOP\\<%= app_name %>.lnk" "$INSTDIR\\<%= app_name %>.vbs" "" "$INSTDIR\\app_icon.ico"
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstall"
|
||||
Delete "$DESKTOP\\<%= app_name %>.lnk"
|
||||
; TODO: stop epmd if it was started
|
||||
RMDir /r "$INSTDIR"
|
||||
SectionEnd
|
||||
"""
|
||||
|
||||
EEx.function_from_string(:defp, :nsi, code, [:options], trim: true)
|
||||
|
||||
code = ~S"""
|
||||
<%
|
||||
app_name = Keyword.fetch!(options, :name)
|
||||
module = Keyword.fetch!(options, :module)
|
||||
%>' This vbs script avoids a flashing cmd window when launching the release bat file
|
||||
|
||||
root = Left(Wscript.ScriptFullName, Len(Wscript.ScriptFullName) - Len(Wscript.ScriptName))
|
||||
script = root & "rel\bin\<%= release.name %>.bat"
|
||||
|
||||
Set shell = CreateObject("WScript.Shell")
|
||||
|
||||
' Below we run two commands:
|
||||
'
|
||||
' 1. bin/release rpc
|
||||
' 2. bin/release start
|
||||
'
|
||||
' The first one will only succeed when the app is already running. The second one when it is not.
|
||||
' It's ok for either to fail because we run them asynchronously.
|
||||
|
||||
Set env = shell.Environment("Process")
|
||||
env("PATH") = ".\rel\vendor\elixir\bin;.\rel\erts-<%= release.erts_version %>\bin;" & env("PATH")
|
||||
|
||||
If WScript.Arguments.Count > 0 Then
|
||||
input = WScript.Arguments(0)
|
||||
Else
|
||||
input = "reopen_app"
|
||||
End If
|
||||
|
||||
' Below, we're basically doing:
|
||||
'
|
||||
' $ bin/release rpc 'AppBuilder.Windows.__send_events__(MyApp, input)'
|
||||
'
|
||||
' We send the input through IO, as opposed using the rpc expression, to avoid RCE.
|
||||
cmd = "echo " & input & " | \""" & script & \""" rpc ""AppBuilder.Windows.__send_events__(<%= inspect(module) %>, String.trim(IO.read(:line)))\"""
|
||||
code = shell.Run("cmd /c " & cmd, 0)
|
||||
|
||||
' Below, we're basically doing:
|
||||
'
|
||||
' $ bin/release start
|
||||
'
|
||||
' We send the input through the environment variable as we can't easily access argv
|
||||
' when booting through the release script.
|
||||
|
||||
If WScript.Arguments.Count > 0 Then
|
||||
env("APP_BUILDER_INPUT") = WScript.Arguments(0)
|
||||
Else
|
||||
env("APP_BUILDER_INPUT") = "new_file"
|
||||
End If
|
||||
|
||||
cmd = \"""" & script & \""" start"
|
||||
code = shell.Run("cmd /c " & cmd & " >> " & root & "\Logs\<%= app_name %>.log 2>&1", 0)
|
||||
"""
|
||||
|
||||
EEx.function_from_string(:defp, :launcher_vbs, code, [:release, :options], trim: true)
|
||||
|
||||
defp ensure_vcredistx64 do
|
||||
url = "https://aka.ms/vs/17/release/vc_redist.x64.exe"
|
||||
sha256 = "426a34c6f10ea8f7da58a8c976b586ad84dd4bab42a0cfdbe941f1763b7755e5"
|
||||
AppBuilder.Utils.ensure_executable(url, sha256)
|
||||
AppBuilder.Utils.ensure_executable(url)
|
||||
end
|
||||
|
||||
defp ensure_makensis do
|
||||
|
@ -259,6 +95,7 @@ defmodule AppBuilder.Windows do
|
|||
end
|
||||
|
||||
defp create_icon(src_path, dest_path) do
|
||||
log(:green, "creating", Path.relative_to_cwd(dest_path))
|
||||
src_path = normalize_icon_path(src_path)
|
||||
|
||||
if Path.extname(src_path) == ".ico" do
|
||||
|
|
12
app_builder/lib/templates/macos/Entitlements.plist.eex
Normal file
12
app_builder/lib/templates/macos/Entitlements.plist.eex
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
67
app_builder/lib/templates/macos/Info.plist.eex
Normal file
67
app_builder/lib/templates/macos/Info.plist.eex
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string><%= @app_options[:name] %>Launcher</string>
|
||||
<key>CFBundleName</key>
|
||||
<string><%= @app_options[:name] %></string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string><%= @app_options[:name] %></string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string><%= @release.version %></string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string><%= @release.version %></string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
|
||||
<%= if schemes = @app_options[:url_schemes] do %>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<%= for scheme <- schemes do %>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string><%= @app_options[:name] %></string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string><%= scheme %></string>
|
||||
</array>
|
||||
</dict>
|
||||
<% end %>
|
||||
</array>
|
||||
<% end %>
|
||||
|
||||
<%= if types = @app_options[:document_types] do %>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<%= for type <- types do %>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string><%= type.name %></string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string><%= type.macos_role %></string>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<%= for ext <- type.extensions do %>
|
||||
<string><%= ext %></string>
|
||||
<% end %>
|
||||
</array>
|
||||
<%= if type[:icon_path] do %>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string><%= type.name %>Icon</string>
|
||||
<% end %>
|
||||
</dict>
|
||||
<% end %>
|
||||
</array>
|
||||
<% end %>
|
||||
|
||||
<%= if @app_options[:macos_is_agent_app] do %>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<% end %>
|
||||
</dict>
|
||||
</plist>
|
54
app_builder/lib/templates/macos/Launcher.swift.eex
Normal file
54
app_builder/lib/templates/macos/Launcher.swift.eex
Normal file
|
@ -0,0 +1,54 @@
|
|||
<%
|
||||
|
||||
additional_paths = [
|
||||
"rel/erts-#{@release.erts_version}/bin"
|
||||
] ++ @app_options[:additional_paths]
|
||||
|
||||
additional_paths = Enum.map_join(additional_paths, ":", &"\\(resourcePath)/#{&1}")
|
||||
|
||||
%>
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
func log(_ line: String) {
|
||||
logFile.write("\(line)\n".data(using: .utf8)!)
|
||||
}
|
||||
|
||||
let fm = FileManager.default
|
||||
let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as! String
|
||||
let home = NSHomeDirectory()
|
||||
let logPath = "\(home)/Library/Logs/\(appName).log"
|
||||
if !fm.fileExists(atPath: logPath) { fm.createFile(atPath: logPath, contents: Data()) }
|
||||
let logFile = FileHandle(forUpdatingAtPath: logPath)!
|
||||
logFile.seekToEndOfFile()
|
||||
|
||||
let releaseScriptPath = Bundle.main.path(forResource: "rel/bin/<%= @release.name %>", ofType: "")!
|
||||
|
||||
let resourcePath = Bundle.main.resourcePath ?? ""
|
||||
let additionalPaths = "<%= additional_paths %>"
|
||||
|
||||
var environment = ProcessInfo.processInfo.environment
|
||||
let path = environment["PATH"] ?? ""
|
||||
environment["PATH"] = "\(additionalPaths):\(path)"
|
||||
|
||||
let task = Process()
|
||||
task.environment = environment
|
||||
task.launchPath = releaseScriptPath
|
||||
task.arguments = ["start"]
|
||||
task.standardOutput = logFile
|
||||
task.standardError = logFile
|
||||
|
||||
log("[\(appName)Launcher] starting release")
|
||||
try task.run()
|
||||
log("[\(appName)Launcher] pid: \(task.processIdentifier)")
|
||||
|
||||
task.waitUntilExit()
|
||||
log("[\(appName)Launcher] release exited with \(task.terminationStatus)")
|
||||
|
||||
if task.terminationStatus != 0 {
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .critical
|
||||
alert.messageText = "\(appName) exited with error status \(task.terminationStatus)."
|
||||
alert.informativeText = "Logs available at: \(logPath)"
|
||||
alert.runModal()
|
||||
}
|
82
app_builder/lib/templates/windows/Installer.nsi.eex
Normal file
82
app_builder/lib/templates/windows/Installer.nsi.eex
Normal file
|
@ -0,0 +1,82 @@
|
|||
<%
|
||||
app_name = Keyword.fetch!(@app_options, :name)
|
||||
%>
|
||||
!include "MUI2.nsh"
|
||||
|
||||
;--------------------------------
|
||||
;General
|
||||
|
||||
Name "<%= app_name %>"
|
||||
OutFile "<%= app_name %>Install.exe"
|
||||
Unicode True
|
||||
InstallDir "$LOCALAPPDATA\<%= app_name %>"
|
||||
|
||||
; Need admin for registering URL scheme
|
||||
RequestExecutionLevel admin
|
||||
|
||||
;--------------------------------
|
||||
;Interface Settings
|
||||
|
||||
!define MUI_ABORTWARNING
|
||||
|
||||
;--------------------------------
|
||||
;Pages
|
||||
|
||||
;!insertmacro MUI_PAGE_COMPONENTS
|
||||
!define MUI_ICON "AppIcon.ico"
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
|
||||
;--------------------------------
|
||||
;Languages
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
;--------------------------------
|
||||
;Installer Sections
|
||||
|
||||
Section "Install"
|
||||
SetOutPath "$INSTDIR"
|
||||
|
||||
File vcredist_x64.exe
|
||||
ExecWait '"$INSTDIR\vcredist_x64.exe" /install'
|
||||
|
||||
File /r rel rel
|
||||
File "<%= app_name %>Launcher.vbs"
|
||||
File "AppIcon.ico"
|
||||
|
||||
CreateDirectory "$INSTDIR\Logs"
|
||||
WriteUninstaller "$INSTDIR\<%= app_name %>Uninstall.exe"
|
||||
|
||||
<%= for type <- Keyword.fetch!(@app_options, :document_types) do %>
|
||||
<%= for ext <- type.extensions do %>
|
||||
WriteRegStr HKCR ".<%= ext %>" "" "<%= app_name %>.<%= type.name %>"
|
||||
<% end %>
|
||||
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>" "" "<%= type.name %>"
|
||||
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>\DefaultIcon" "" "$INSTDIR\<%= type.name %>Icon.ico"
|
||||
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>\shell\open\command" "" '$WINDIR\system32\wscript.exe "$INSTDIR\<%= app_name %>Launcher.vbs" "open_file:%1"'
|
||||
<% end %>
|
||||
|
||||
<%= for url_scheme <- Keyword.fetch!(@app_options, :url_schemes) do %>
|
||||
DetailPrint "Register <%= url_scheme %> URL Handler"
|
||||
DeleteRegKey HKCR "<%= url_scheme %>"
|
||||
WriteRegStr HKCR "<%= url_scheme %>" "" "<%= url_scheme %> Protocol"
|
||||
WriteRegStr HKCR "<%= url_scheme %>" "URL Protocol" ""
|
||||
WriteRegStr HKCR "<%= url_scheme %>\shell" "" ""
|
||||
WriteRegStr HKCR "<%= url_scheme %>\shell\open" "" ""
|
||||
WriteRegStr HKCR "<%= url_scheme %>\shell\open\command" "" '$WINDIR\system32\wscript.exe "$INSTDIR\<%= app_name %>Launcher.vbs" "open_url:%1"'
|
||||
<% end %>
|
||||
SectionEnd
|
||||
|
||||
Section "Desktop Shortcut"
|
||||
CreateShortCut "$DESKTOP\<%= app_name %>.lnk" "$INSTDIR\<%= app_name %>Launcher.vbs" "" "$INSTDIR\AppIcon.ico"
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstall"
|
||||
Delete "$DESKTOP\<%= app_name %>.lnk"
|
||||
; TODO: stop epmd if it was started
|
||||
RMDir /r "$INSTDIR"
|
||||
SectionEnd
|
48
app_builder/lib/templates/windows/Launcher.vbs.eex
Normal file
48
app_builder/lib/templates/windows/Launcher.vbs.eex
Normal file
|
@ -0,0 +1,48 @@
|
|||
' This vbs script avoids a flashing cmd window when launching the release bat file
|
||||
|
||||
root = Left(Wscript.ScriptFullName, Len(Wscript.ScriptFullName) - Len(Wscript.ScriptName))
|
||||
script = root & "rel\bin\<%= @release.name %>.bat"
|
||||
|
||||
Set shell = CreateObject("WScript.Shell")
|
||||
|
||||
' Below we run two commands:
|
||||
'
|
||||
' 1. bin/release rpc
|
||||
' 2. bin/release start
|
||||
'
|
||||
' The first one will only succeed when the app is already running. The second one when it is not.
|
||||
' It's ok for either to fail because we run them asynchronously.
|
||||
|
||||
Set env = shell.Environment("Process")
|
||||
env("PATH") = "<%= Enum.map_join(@app_options[:additional_paths], ";", &String.replace(&1, "/", "\\")) %>" & env("PATH")
|
||||
|
||||
If WScript.Arguments.Count > 0 Then
|
||||
input = WScript.Arguments(0)
|
||||
Else
|
||||
input = "reopen_app"
|
||||
End If
|
||||
|
||||
' Below, we're basically doing:
|
||||
'
|
||||
' $ bin/release rpc 'AppBuilder.Windows.__send_events__(MyApp, input)'
|
||||
'
|
||||
' We send the input through IO, as opposed using the rpc expression, to avoid RCE.
|
||||
cmd = "echo " & input & " | """ & script & """ rpc ""AppBuilder.Windows.__send_events__(<%= inspect(@app_options[:server]) %>, String.trim(IO.read(:line)))"""
|
||||
code = shell.Run("cmd /c " & cmd, 0)
|
||||
|
||||
' Below, we're basically doing:
|
||||
'
|
||||
' $ bin/release start
|
||||
'
|
||||
' We send the input through the environment variable as we can't easily access argv
|
||||
' when booting through the release script.
|
||||
|
||||
If WScript.Arguments.Count > 0 Then
|
||||
env("APP_BUILDER_INPUT") = WScript.Arguments(0)
|
||||
Else
|
||||
env("APP_BUILDER_INPUT") = "new_file"
|
||||
End If
|
||||
|
||||
cmd = """" & script & """ start"
|
||||
code = shell.Run("cmd /c " & cmd & " >> " & root & "\Logs\<%= @app_options[:name] %>.log 2>&1", 0)
|
||||
|
9
app_builder/lib/templates/windows/Manifest.xml.eex
Normal file
9
app_builder/lib/templates/windows/Manifest.xml.eex
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
115
mix.exs
115
mix.exs
|
@ -126,6 +126,8 @@ defmodule Livebook.MixProject do
|
|||
## Releases
|
||||
|
||||
defp releases do
|
||||
macos_notarization = macos_notarization()
|
||||
|
||||
[
|
||||
livebook: [
|
||||
include_executables_for: [:unix],
|
||||
|
@ -133,32 +135,58 @@ defmodule Livebook.MixProject do
|
|||
rel_templates_path: "rel/server",
|
||||
steps: [:assemble, &remove_cookie/1]
|
||||
],
|
||||
mac_app: [
|
||||
include_executables_for: [:unix],
|
||||
include_erts: false,
|
||||
rel_templates_path: "rel/app",
|
||||
steps: [:assemble, &remove_cookie/1, &standalone_erlang_elixir/1, &build_mac_app/1]
|
||||
],
|
||||
mac_app_dmg: [
|
||||
include_executables_for: [:unix],
|
||||
include_erts: false,
|
||||
rel_templates_path: "rel/app",
|
||||
steps: [:assemble, &remove_cookie/1, &standalone_erlang_elixir/1, &build_mac_app_dmg/1]
|
||||
],
|
||||
windows_installer: [
|
||||
include_executables_for: [:windows],
|
||||
app: [
|
||||
include_erts: false,
|
||||
rel_templates_path: "rel/app",
|
||||
steps: [
|
||||
:assemble,
|
||||
&remove_cookie/1,
|
||||
&standalone_erlang_elixir/1,
|
||||
&build_windows_installer/1
|
||||
&AppBuilder.bundle/1
|
||||
],
|
||||
app: [
|
||||
name: "Livebook",
|
||||
icon_path: [
|
||||
macos: "rel/app/icon-macos.png",
|
||||
windows: "rel/app/icon.ico"
|
||||
],
|
||||
url_schemes: ["livebook"],
|
||||
document_types: [
|
||||
%{
|
||||
name: "LiveMarkdown",
|
||||
extensions: ["livemd"],
|
||||
icon_path: [
|
||||
macos: "rel/app/icon.png",
|
||||
windows: "rel/app/icon.ico"
|
||||
],
|
||||
macos_role: "Editor"
|
||||
}
|
||||
],
|
||||
additional_paths: [
|
||||
"rel/erts-#{:erlang.system_info(:version)}/bin",
|
||||
"rel/vendor/elixir/bin"
|
||||
],
|
||||
server: LivebookApp,
|
||||
macos_is_agent_app: true,
|
||||
macos_build_dmg: macos_notarization != nil,
|
||||
macos_notarization: macos_notarization,
|
||||
windows_build_installer: true
|
||||
]
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
defp macos_notarization do
|
||||
identity = System.get_env("NOTARIZE_IDENTITY")
|
||||
team_id = System.get_env("NOTARIZE_TEAM_ID")
|
||||
apple_id = System.get_env("NOTARIZE_APPLE_ID")
|
||||
password = System.get_env("NOTARIZE_PASSWORD")
|
||||
|
||||
if identity && team_id && apple_id && password do
|
||||
[identity: identity, team_id: team_id, apple_id: apple_id, password: password]
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_cookie(release) do
|
||||
File.rm!(Path.join(release.path, "releases/COOKIE"))
|
||||
release
|
||||
|
@ -171,61 +199,4 @@ defmodule Livebook.MixProject do
|
|||
|> Standalone.copy_otp()
|
||||
|> Standalone.copy_elixir(@app_elixir_version)
|
||||
end
|
||||
|
||||
@app_options [
|
||||
name: "Livebook",
|
||||
version: @version,
|
||||
icon_path: [
|
||||
macos: "rel/app/icon-macos.png",
|
||||
windows: "rel/app/icon.ico"
|
||||
],
|
||||
additional_paths: [
|
||||
"/rel/erts-#{:erlang.system_info(:version)}/bin",
|
||||
"/rel/vendor/elixir/bin"
|
||||
],
|
||||
url_schemes: ["livebook"],
|
||||
document_types: [
|
||||
%{
|
||||
name: "LiveMarkdown",
|
||||
extensions: ["livemd"],
|
||||
icon_path: [
|
||||
macos: "rel/app/icon.png",
|
||||
windows: "rel/app/icon.ico"
|
||||
],
|
||||
# macos specific
|
||||
role: "Editor"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
defp build_mac_app(release) do
|
||||
options =
|
||||
[
|
||||
is_agent_app: true
|
||||
] ++ @app_options
|
||||
|
||||
AppBuilder.build_mac_app(release, options)
|
||||
end
|
||||
|
||||
defp build_mac_app_dmg(release) do
|
||||
options =
|
||||
[
|
||||
is_agent_app: true,
|
||||
codesign: [
|
||||
identity: System.fetch_env!("CODESIGN_IDENTITY")
|
||||
],
|
||||
notarize: [
|
||||
team_id: System.fetch_env!("NOTARIZE_TEAM_ID"),
|
||||
apple_id: System.fetch_env!("NOTARIZE_APPLE_ID"),
|
||||
password: System.fetch_env!("NOTARIZE_PASSWORD")
|
||||
]
|
||||
] ++ @app_options
|
||||
|
||||
AppBuilder.build_mac_app_dmg(release, options)
|
||||
end
|
||||
|
||||
defp build_windows_installer(release) do
|
||||
options = Keyword.drop(@app_options, [:additional_paths]) ++ [module: LivebookApp]
|
||||
AppBuilder.build_windows_installer(release, options)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Reference in a new issue