mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-06 04:54:29 +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
|
fi
|
||||||
|
|
||||||
export PATH=$PWD/tmp/wxwidgets-${wxwidgets_vsn}-$target/bin:$PATH
|
export PATH=$PWD/tmp/wxwidgets-${wxwidgets_vsn}-$target/bin:$PATH
|
||||||
echo "wx"
|
echo "checking wx"
|
||||||
file `which wxrc`
|
file `which wxrc`
|
||||||
wx-config --version
|
wx-config --version
|
||||||
echo
|
echo
|
||||||
|
@ -29,7 +29,7 @@ main() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export PATH=$PWD/tmp/otp-${otp_vsn}-$target/bin:$PATH
|
export PATH=$PWD/tmp/otp-${otp_vsn}-$target/bin:$PATH
|
||||||
echo "otp"
|
echo "checking otp"
|
||||||
file `which erlc`
|
file `which erlc`
|
||||||
erl +V
|
erl +V
|
||||||
erl -noshell -eval 'ok = crypto:start(), io:format("crypto ok~n"), halt().'
|
erl -noshell -eval 'ok = crypto:start(), io:format("crypto ok~n"), halt().'
|
||||||
|
@ -41,7 +41,7 @@ main() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export PATH=$PWD/tmp/elixir-${elixir_vsn}/bin:$PATH
|
export PATH=$PWD/tmp/elixir-${elixir_vsn}/bin:$PATH
|
||||||
echo "elixir"
|
echo "checking elixir"
|
||||||
elixir --version
|
elixir --version
|
||||||
|
|
||||||
cat << EOF > tmp/bootstrap_env.sh
|
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:
|
# Usage:
|
||||||
#
|
#
|
||||||
# $ sh .github/scripts/app/build_mac.sh
|
# $ 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 livebook://github.com/livebook-dev/livebook/blob/main/test/support/notebooks/basic.livemd
|
||||||
# $ open ./test/support/notebooks/basic.livemd
|
# $ open ./test/support/notebooks/basic.livemd
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
sh .github/scripts/app/bootstrap_mac.sh
|
sh .github/scripts/app/bootstrap_mac.sh
|
||||||
sh tmp/bootstrap_env.sh
|
. tmp/bootstrap_env.sh
|
||||||
|
MIX_ENV=prod MIX_TARGET=app mix release app --overwrite
|
||||||
# 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
|
|
||||||
|
|
4
.github/scripts/app/build_windows.sh
vendored
4
.github/scripts/app/build_windows.sh
vendored
|
@ -3,9 +3,9 @@
|
||||||
# Usage:
|
# Usage:
|
||||||
#
|
#
|
||||||
# $ sh .github/scripts/app/build_windows.sh
|
# $ 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 livebook://github.com/livebook-dev/livebook/blob/main/test/support/notebooks/basic.livemd
|
||||||
# $ start ./test/support/notebooks/basic.livemd
|
# $ start ./test/support/notebooks/basic.livemd
|
||||||
set -e
|
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
|
@wx_id_osx_hide 5250
|
||||||
|
|
||||||
def start_link(_) do
|
def start_link(_) do
|
||||||
{:wx_ref, _, _, pid} = :wx_object.start_link(__MODULE__, [], [])
|
{:wx_ref, _, _, pid} = :wx_object.start_link({:local, __MODULE__}, __MODULE__, [], [])
|
||||||
{:ok, pid}
|
{:ok, pid}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -47,11 +47,9 @@ defmodule WxDemo.Window do
|
||||||
@impl true
|
@impl true
|
||||||
def init(_) do
|
def init(_) do
|
||||||
app_name = "WxDemo"
|
app_name = "WxDemo"
|
||||||
|
|
||||||
true = Process.register(self(), __MODULE__)
|
|
||||||
os = os()
|
os = os()
|
||||||
wx = :wx.new()
|
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
|
if os == :macos do
|
||||||
fixup_macos_menubar(frame, app_name)
|
fixup_macos_menubar(frame, app_name)
|
||||||
|
|
|
@ -26,51 +26,41 @@ defmodule WxDemo.MixProject do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp releases do
|
defp releases do
|
||||||
options = [
|
macos_notarization = macos_notarization()
|
||||||
name: "WxDemo",
|
|
||||||
url_schemes: ["wxdemo"],
|
|
||||||
document_types: [
|
|
||||||
%{
|
|
||||||
name: "WxDemo",
|
|
||||||
extensions: ["wxdemo"],
|
|
||||||
# macos specific
|
|
||||||
role: "Editor"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
[
|
[
|
||||||
mac_app: [
|
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],
|
|
||||||
steps: [
|
steps: [
|
||||||
:assemble,
|
: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
|
end
|
||||||
|
|
||||||
defp build_mac_app_dmg(release, options) do
|
defp macos_notarization do
|
||||||
options =
|
identity = System.get_env("NOTARIZE_IDENTITY")
|
||||||
[
|
team_id = System.get_env("NOTARIZE_TEAM_ID")
|
||||||
codesign: [
|
apple_id = System.get_env("NOTARIZE_APPLE_ID")
|
||||||
identity: System.fetch_env!("CODESIGN_IDENTITY")
|
password = System.get_env("NOTARIZE_PASSWORD")
|
||||||
],
|
|
||||||
notarize: [
|
|
||||||
team_id: System.fetch_env!("NOTARIZE_TEAM_ID"),
|
|
||||||
apple_id: System.fetch_env!("NOTARIZE_APPLE_ID"),
|
|
||||||
password: System.fetch_env!("NOTARIZE_PASSWORD")
|
|
||||||
]
|
|
||||||
] ++ options
|
|
||||||
|
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +1,32 @@
|
||||||
defmodule AppBuilder do
|
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
|
def os do
|
||||||
case :os.type() do
|
case :os.type() do
|
||||||
|
|
|
@ -3,74 +3,120 @@ defmodule AppBuilder.MacOS do
|
||||||
|
|
||||||
import AppBuilder.Utils
|
import AppBuilder.Utils
|
||||||
|
|
||||||
def build_mac_app_dmg(release, options) do
|
@templates_path "#{__ENV__.file}/../../templates"
|
||||||
{codesign, options} = Keyword.pop(options, :codesign)
|
|
||||||
{notarize, options} = Keyword.pop(options, :notarize)
|
|
||||||
|
|
||||||
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)
|
app_name = Keyword.fetch!(options, :name)
|
||||||
File.rm_rf!("tmp/dmg")
|
notarization = Keyword.fetch!(options, :macos_notarization)
|
||||||
File.mkdir_p!("tmp/dmg")
|
|
||||||
File.ln_s!("/Applications", "tmp/dmg/Applications")
|
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!(
|
File.cp_r!(
|
||||||
Path.join([Mix.Project.build_path(), "rel", "#{app_name}.app"]),
|
"#{Mix.Project.build_path()}/#{app_name}.app",
|
||||||
"tmp/dmg/#{app_name}.app"
|
app_dir
|
||||||
)
|
)
|
||||||
|
|
||||||
to_sign =
|
to_sign =
|
||||||
"tmp/dmg/#{app_name}.app/**"
|
"#{app_dir}/**"
|
||||||
|> Path.wildcard()
|
|> Path.wildcard()
|
||||||
|> Enum.filter(fn file ->
|
|> Enum.filter(fn file ->
|
||||||
stat = File.lstat!(file)
|
stat = File.lstat!(file)
|
||||||
Bitwise.band(0o100, stat.mode) != 0 and stat.type == :regular
|
Bitwise.band(0o100, stat.mode) != 0 and stat.type == :regular
|
||||||
end)
|
end)
|
||||||
|
|
||||||
to_sign = to_sign ++ ["tmp/dmg/#{app_name}.app"]
|
to_sign = to_sign ++ [app_dir]
|
||||||
|
|
||||||
if codesign do
|
entitlements_eex_path = "#{Path.expand(@templates_path)}/macos/Entitlements.plist.eex"
|
||||||
entitlements_path = "tmp/entitlements.plist"
|
entitlements_plist_path = "#{tmp_dir}/Entitlements.plist"
|
||||||
File.write!(entitlements_path, entitlements())
|
|
||||||
codesign(to_sign, "--options=runtime --entitlements=#{entitlements_path}", codesign)
|
copy_template(entitlements_eex_path, entitlements_plist_path,
|
||||||
end
|
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()
|
arch = :erlang.system_info(:system_architecture) |> to_string |> String.split("-") |> hd()
|
||||||
vsn = release.version
|
vsn = release.version
|
||||||
basename = "#{app_name}-#{vsn}-#{arch}.dmg"
|
dmg_path = "#{Mix.Project.build_path()}/#{app_name}Install-#{vsn}-#{arch}.dmg"
|
||||||
|
log(:green, "creating", Path.relative_to_cwd(dmg_path))
|
||||||
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)
|
|
||||||
|
|
||||||
cmd!(
|
cmd!(
|
||||||
"hdiutil",
|
"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!(
|
log(:green, "notarizing", Path.relative_to_cwd(dmg_path))
|
||||||
"hdiutil",
|
notarize(dmg_path, notarization)
|
||||||
~w(convert #{tmp_dmg_path} -format UDZO -o #{dmg_path})
|
|
||||||
)
|
|
||||||
|
|
||||||
if codesign do
|
|
||||||
codesign([dmg_path], "", codesign)
|
|
||||||
end
|
|
||||||
|
|
||||||
if notarize do
|
|
||||||
notarize(dmg_path, notarize)
|
|
||||||
end
|
|
||||||
|
|
||||||
File.rm!(tmp_dmg_path)
|
|
||||||
release
|
release
|
||||||
end
|
end
|
||||||
|
|
||||||
defp codesign(paths, args, options) do
|
defp codesign(paths, extra_flags, options) do
|
||||||
identity = Keyword.fetch!(options, :identity)
|
identity = Keyword.fetch!(options, :identity)
|
||||||
paths = Enum.join(paths, " ")
|
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
|
end
|
||||||
|
|
||||||
defp notarize(path, options) do
|
defp notarize(path, options) do
|
||||||
|
@ -89,106 +135,8 @@ defmodule AppBuilder.MacOS do
|
||||||
""")
|
""")
|
||||||
end
|
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
|
defp create_icon(src_path, dest_path) do
|
||||||
|
log(:green, :creating, Path.relative_to_cwd(dest_path))
|
||||||
src_path = normalize_icon_path(src_path)
|
src_path = normalize_icon_path(src_path)
|
||||||
|
|
||||||
if Path.extname(src_path) == ".icns" do
|
if Path.extname(src_path) == ".icns" do
|
||||||
|
@ -210,7 +158,7 @@ defmodule AppBuilder.MacOS do
|
||||||
|
|
||||||
size = size * scale
|
size = size * scale
|
||||||
out = "#{dest_tmp_path}/icon_#{size}x#{size}#{suffix}.png"
|
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
|
end
|
||||||
|
|
||||||
cmd!("iconutil", ~w(-c icns #{dest_tmp_path} -o #{dest_path}))
|
cmd!("iconutil", ~w(-c icns #{dest_tmp_path} -o #{dest_path}))
|
||||||
|
@ -227,101 +175,4 @@ defmodule AppBuilder.MacOS do
|
||||||
"arm64-apple-macosx12"
|
"arm64-apple-macosx12"
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -5,7 +5,11 @@ defmodule AppBuilder.Utils do
|
||||||
|
|
||||||
def cmd!(bin, args, opts \\ []) do
|
def cmd!(bin, args, opts \\ []) do
|
||||||
opts = Keyword.put_new(opts, :into, IO.stream())
|
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
|
end
|
||||||
|
|
||||||
def shell!(command, opts \\ []) do
|
def shell!(command, opts \\ []) do
|
||||||
|
@ -13,11 +17,17 @@ defmodule AppBuilder.Utils do
|
||||||
{_, 0} = System.shell(command, opts)
|
{_, 0} = System.shell(command, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_executable(url) do
|
||||||
|
ensure_executable(url, :no_verify)
|
||||||
|
end
|
||||||
|
|
||||||
def ensure_executable(url, expected_sha256) do
|
def ensure_executable(url, expected_sha256) do
|
||||||
tmp_dir = Path.join(System.tmp_dir!(), Path.basename(url, Path.extname(url)))
|
tmp_dir = Path.join(System.tmp_dir!(), Path.basename(url, Path.extname(url)))
|
||||||
path = Path.join(tmp_dir, Path.basename(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)
|
File.mkdir_p!(tmp_dir)
|
||||||
body = download_and_verify(url, expected_sha256)
|
body = download_and_verify(url, expected_sha256)
|
||||||
File.write!(path, body)
|
File.write!(path, body)
|
||||||
|
@ -53,6 +63,10 @@ defmodule AppBuilder.Utils do
|
||||||
body
|
body
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp verify(data, :no_verify) do
|
||||||
|
data
|
||||||
|
end
|
||||||
|
|
||||||
defp verify(data, expected_sha256) do
|
defp verify(data, expected_sha256) do
|
||||||
actual_sha256 = :crypto.hash(:sha256, data) |> Base.encode16(case: :lower)
|
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
|
def normalize_icon_path(path_per_os) when is_list(path_per_os) do
|
||||||
Keyword.fetch!(path_per_os, AppBuilder.os())
|
Keyword.fetch!(path_per_os, AppBuilder.os())
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -2,7 +2,52 @@ defmodule AppBuilder.Windows do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
import AppBuilder.Utils
|
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)
|
def __send_events__(server, input)
|
||||||
|
|
||||||
|
@ -27,218 +72,9 @@ defmodule AppBuilder.Windows do
|
||||||
send(server, {:open_file, path})
|
send(server, {:open_file, path})
|
||||||
end
|
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
|
defp ensure_vcredistx64 do
|
||||||
url = "https://aka.ms/vs/17/release/vc_redist.x64.exe"
|
url = "https://aka.ms/vs/17/release/vc_redist.x64.exe"
|
||||||
sha256 = "426a34c6f10ea8f7da58a8c976b586ad84dd4bab42a0cfdbe941f1763b7755e5"
|
AppBuilder.Utils.ensure_executable(url)
|
||||||
AppBuilder.Utils.ensure_executable(url, sha256)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp ensure_makensis do
|
defp ensure_makensis do
|
||||||
|
@ -259,6 +95,7 @@ defmodule AppBuilder.Windows do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp create_icon(src_path, dest_path) do
|
defp create_icon(src_path, dest_path) do
|
||||||
|
log(:green, "creating", Path.relative_to_cwd(dest_path))
|
||||||
src_path = normalize_icon_path(src_path)
|
src_path = normalize_icon_path(src_path)
|
||||||
|
|
||||||
if Path.extname(src_path) == ".ico" do
|
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
|
## Releases
|
||||||
|
|
||||||
defp releases do
|
defp releases do
|
||||||
|
macos_notarization = macos_notarization()
|
||||||
|
|
||||||
[
|
[
|
||||||
livebook: [
|
livebook: [
|
||||||
include_executables_for: [:unix],
|
include_executables_for: [:unix],
|
||||||
|
@ -133,32 +135,58 @@ defmodule Livebook.MixProject do
|
||||||
rel_templates_path: "rel/server",
|
rel_templates_path: "rel/server",
|
||||||
steps: [:assemble, &remove_cookie/1]
|
steps: [:assemble, &remove_cookie/1]
|
||||||
],
|
],
|
||||||
mac_app: [
|
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],
|
|
||||||
include_erts: false,
|
include_erts: false,
|
||||||
rel_templates_path: "rel/app",
|
rel_templates_path: "rel/app",
|
||||||
steps: [
|
steps: [
|
||||||
:assemble,
|
:assemble,
|
||||||
&remove_cookie/1,
|
&remove_cookie/1,
|
||||||
&standalone_erlang_elixir/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
|
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
|
defp remove_cookie(release) do
|
||||||
File.rm!(Path.join(release.path, "releases/COOKIE"))
|
File.rm!(Path.join(release.path, "releases/COOKIE"))
|
||||||
release
|
release
|
||||||
|
@ -171,61 +199,4 @@ defmodule Livebook.MixProject do
|
||||||
|> Standalone.copy_otp()
|
|> Standalone.copy_otp()
|
||||||
|> Standalone.copy_elixir(@app_elixir_version)
|
|> Standalone.copy_elixir(@app_elixir_version)
|
||||||
end
|
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
|
end
|
||||||
|
|
Loading…
Add table
Reference in a new issue