Add Mac app release (#865)

This commit is contained in:
Wojtek Mach 2022-01-17 17:34:38 +01:00 committed by GitHub
parent 4d79706c01
commit e8c80bf6a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 891 additions and 33 deletions

142
.github/scripts/app/bootstrap_mac.sh vendored Normal file
View file

@ -0,0 +1,142 @@
#!/bin/bash
set -e
main() {
export MAKEFLAGS=-j$(getconf _NPROCESSORS_ONLN)
wxwidgets_vsn="3.1.5"
otp_vsn="24.2"
elixir_vsn="1.13.1"
target=$(target)
mkdir -p tmp
if [ ! -d "tmp/wxwidgets-${wxwidgets_vsn}-$target" ]; then
build_wxwidgets $wxwidgets_vsn $target
fi
export PATH=$PWD/tmp/wxwidgets-${wxwidgets_vsn}-$target/bin:$PATH
echo "wx"
file `which wxrc`
wx-config --version
echo
openssl_dir=$(brew --prefix openssl@1.1)
if [ ! -d "tmp/otp-${otp_vsn}-$target" ]; then
build_otp $otp_vsn $target $openssl_dir
fi
export PATH=$PWD/tmp/otp-${otp_vsn}-$target/bin:$PATH
echo "otp"
file `which erlc`
erl +V
erl -noshell -eval 'ok = crypto:start(), io:format("crypto ok~n"), halt().'
erl -noshell -eval '{wx_ref,_,_,_} = wx:new(), io:format("wx ok~n"), halt().'
echo
if [ ! -d "tmp/elixir-${elixir_vsn}" ]; then
build_elixir $elixir_vsn
fi
export PATH=$PWD/tmp/elixir-${elixir_vsn}/bin:$PATH
echo "elixir"
elixir --version
cat << EOF > tmp/bootstrap_env.sh
export PATH=\$PWD/tmp/otp-${otp_vsn}-${target}/bin:\$PATH
export PATH=\$PWD/tmp/elixir-${elixir_vsn}/bin:\$PATH
EOF
}
build_wxwidgets() {
vsn=$1
target=$2
otp_bootstrap_root=$PWD
cd tmp
if [ ! -d wxwidgets-$vsn-src ]; then
url=https://github.com/wxWidgets/wxWidgets/releases/download/v$vsn/wxWidgets-$vsn.tar.bz2
echo downloading $url
curl --fail -LO $url
tar -xf wxWidgets-$vsn.tar.bz2
mv wxWidgets-$vsn wxwidgets-$vsn-src
fi
cd wxwidgets-$vsn-src
./configure \
--disable-shared \
--prefix=$otp_bootstrap_root/tmp/wxwidgets-$vsn-$target \
--with-cocoa \
--with-macosx-version-min=10.15 \
--with-libjpeg=builtin \
--with-libtiff=builtin \
--with-libpng=builtin \
--with-liblzma=builtin \
--with-zlib=builtin \
--with-expat=builtin
make
make install
cd $otp_bootstrap_root
}
build_otp() {
vsn=$1
target=$2
openssl_dir=$3
otp_bootstrap_root=$PWD
cd tmp
curl --fail -LO https://github.com/erlang/otp/releases/download/OTP-${vsn}/otp_src_${vsn}.tar.gz
tar -xf otp_src_${vsn}.tar.gz
cd otp_src_${vsn}
export ERL_TOP=`pwd`
export RELEASE_ROOT=$otp_bootstrap_root/tmp/otp-$vsn-$target
./otp_build configure \
--disable-dynamic-ssl-lib \
--with-ssl=$openssl_dir
./otp_build boot -a
./otp_build release -a $RELEASE_ROOT
make release_docs DOC_TARGETS=chunks
cd $RELEASE_ROOT
./Install -sasl $PWD
./bin/erl -noshell -eval 'io:format("~s", [erlang:system_info(system_version)]), halt().'
./bin/erl -noshell -eval 'ok = crypto:start(), halt().'
./bin/erl -noshell -eval '{wx_ref,_,_,_} = wx:new(), halt().'
cd ../..
}
build_elixir() {
vsn=$1
otp_release=$(erl -noshell -eval 'io:format("~s", [erlang:system_info(otp_release)]), halt().')
cd tmp
# TODO: On Elixir 1.14, use https://github.com/elixir-lang/elixir/releases/download/v${vsn}/elixir-${vsn}-otp-${otp_release}.zip
url=https://repo.hex.pm/builds/elixir/v${vsn}-otp-${otp_release}.zip
curl --fail -LO $url
mkdir elixir-$vsn
unzip v${vsn}-otp-${otp_release}.zip -d elixir-$vsn
cd ..
}
target() {
os=$(uname -s | tr '[:upper:]' '[:lower:]')
arch=$(uname -m)
case $arch in
"arm64") arch="aarch64";;
*) ;;
esac
echo "$arch-$os"
}
main

19
.github/scripts/app/build_mac.sh vendored Normal file
View file

@ -0,0 +1,19 @@
#!/bin/bash
# Usage:
#
# $ sh .github/scripts/app/build_mac.sh
# $ open _build/app_prod/rel/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/bootstrap_mac.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

View file

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

26
app_builder/.gitignore vendored Normal file
View file

@ -0,0 +1,26 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
app_builder-*.tar
# Temporary files, for example, from tests.
/tmp/

1
app_builder/README.md Normal file
View file

@ -0,0 +1 @@
# AppBuilder

View file

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

28
app_builder/examples/wx_demo/.gitignore vendored Normal file
View file

@ -0,0 +1,28 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
wx_demo-*.tar
# Temporary files, for example, from tests.
/tmp/
/test.sh

View file

@ -0,0 +1 @@
# WxDemo

View file

@ -0,0 +1,104 @@
defmodule WxDemo.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
WxDemo.Window
]
opts = [strategy: :one_for_one, name: WxDemo.Supervisor]
Supervisor.start_link(children, opts)
end
end
defmodule WxDemo.Window do
@moduledoc false
@behaviour :wx_object
# https://github.com/erlang/otp/blob/OTP-24.1.2/lib/wx/include/wx.hrl#L1314
@wx_id_exit 5006
def start_link(_) do
{:wx_ref, _, _, pid} = :wx_object.start_link(__MODULE__, [], [])
{:ok, pid}
end
def child_spec(init_arg) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [init_arg]},
restart: :transient
}
end
@impl true
def init(_) do
title = "WxDemo"
wx = :wx.new()
frame = :wxFrame.new(wx, -1, title)
if macOS?() do
fixup_macos_menubar(frame, title)
end
:wxFrame.show(frame)
:wxFrame.connect(frame, :command_menu_selected)
:wxFrame.connect(frame, :close_window, skip: true)
:wx.subscribe_events()
state = %{frame: frame}
{frame, state}
end
@impl true
def handle_event({:wx, @wx_id_exit, _, _, _}, state) do
:init.stop()
{:stop, :normal, state}
end
@impl true
def handle_event({:wx, _, _, _, {:wxClose, :close_window}}, state) do
:init.stop()
{:stop, :normal, state}
end
@impl true
def handle_info({:open_url, url}, state) do
:wxMessageDialog.new(state.frame, inspect(url))
|> :wxDialog.showModal()
{:noreply, state}
end
@impl true
# ignore other events
def handle_info(_event, state) do
{:noreply, state}
end
defp fixup_macos_menubar(frame, title) do
menubar = :wxMenuBar.new()
# :wxMenuBar.setAutoWindowMenu(false)
:wxFrame.setMenuBar(frame, menubar)
# App Menu
menu = :wxMenuBar.oSXGetAppleMenu(menubar)
# Remove all items except for quit
for item <- :wxMenu.getMenuItems(menu) do
if :wxMenuItem.getId(item) == @wx_id_exit do
:wxMenuItem.setText(item, "Quit #{title}\tCtrl+Q")
else
:wxMenu.delete(menu, item)
end
end
end
defp macOS?() do
:os.type() == {:unix, :darwin}
end
end

View file

@ -0,0 +1,61 @@
defmodule WxDemo.MixProject do
use Mix.Project
def project do
[
app: :wx_demo,
version: "0.1.0",
elixir: "~> 1.13",
start_permanent: Mix.env() == :prod,
deps: deps(),
releases: releases()
]
end
def application do
[
extra_applications: [:wx, :logger],
mod: {WxDemo.Application, []}
]
end
defp deps do
[
{:app_builder, path: "../.."}
]
end
defp releases do
options = [
name: "WxDemo",
url_schemes: ["wxdemo"]
]
[
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)]
]
]
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
AppBuilder.build_mac_app_dmg(release, options)
end
end

View file

@ -0,0 +1 @@
ExUnit.start()

View file

@ -0,0 +1,3 @@
defmodule WxDemoTest do
use ExUnit.Case, async: true
end

View file

@ -0,0 +1,5 @@
defmodule AppBuilder do
defdelegate build_mac_app(release, options), to: AppBuilder.MacOS
defdelegate build_mac_app_dmg(release, options), to: AppBuilder.MacOS
end

View file

@ -0,0 +1,242 @@
defmodule AppBuilder.MacOS do
@moduledoc false
import AppBuilder.Utils
def build_mac_app_dmg(release, options) do
{codesign, options} = Keyword.pop(options, :codesign)
{notarize, options} = Keyword.pop(options, :notarize)
release = build_mac_app(release, options)
app_name = Keyword.fetch!(options, :name)
File.rm_rf!("tmp/dmg")
File.mkdir_p!("tmp/dmg")
File.ln_s!("/Applications", "tmp/dmg/Applications")
File.cp_r!(
Path.join([Mix.Project.build_path(), "rel", "#{app_name}.app"]),
"tmp/dmg/#{app_name}.app"
)
to_sign =
"tmp/dmg/#{app_name}.app/**"
|> 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"]
if codesign do
entitlements_path = "tmp/entitlements.plist"
File.write!(entitlements_path, """
<?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>
""")
codesign(to_sign, "--options=runtime --entitlements=#{entitlements_path}", codesign)
end
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)
cmd!(
"hdiutil",
~w(create #{tmp_dmg_path} -ov -volname #{app_name}Install -fs HFS+ -srcfolder tmp/dmg)
)
cmd!(
"hdiutil",
~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
end
defp codesign(paths, args, options) do
identity = Keyword.fetch!(options, :identity)
paths = Enum.join(paths, " ")
shell!("codesign --force --timestamp --verbose=4 --sign=\"#{identity}\" #{args} #{paths}")
end
defp notarize(path, options) do
team_id = Keyword.fetch!(options, :team_id)
apple_id = Keyword.fetch!(options, :apple_id)
password = Keyword.fetch!(options, :password)
shell!("""
xcrun notarytool submit \
--team-id "#{team_id}" \
--apple-id "#{apple_id}" \
--password "#{password}" \
--progress \
--wait \
#{path}
""")
end
def build_mac_app(release, options) do
options =
Keyword.validate!(options, [
:name,
:launcher_script,
:logo_path,
:info_plist,
:url_schemes,
:document_types
])
app_name = Keyword.fetch!(options, :name)
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"]))
launcher_script = options[:launcher_script] || launcher_script(release.name, app_name)
launcher_script_path = Path.join([app_bundle_path, "Contents", "MacOS", app_name])
File.mkdir_p!(Path.dirname(launcher_script_path))
File.write!(launcher_script_path, launcher_script)
File.chmod!(launcher_script_path, 0o700)
logo_path = options[:logo_path] || Application.app_dir(:wx, "examples/demo/erlang.png")
create_logo(app_bundle_path, logo_path)
info_plist = options[:info_plist] || build_info_plist(options)
File.write!(Path.join([app_bundle_path, "Contents", "Info.plist"]), info_plist)
release
end
defp launcher_script(release_name, app_name) do
"""
#!/bin/sh
set -e
root=$(dirname $(dirname "$0"))
$root/Resources/rel/bin/#{release_name} start \
1>> ~/Library/Logs/#{app_name}.stdout.log \
2>> ~/Library/Logs/#{app_name}.stderr.log
"""
end
defp build_info_plist(options) do
app_name = Keyword.fetch!(options, :name)
url_schemes =
"""
\n<key>CFBundleURLTypes</key>
<array>
""" <>
for scheme <- options[:url_schemes] || [], into: "" do
"""
<dict>
<key>CFBundleURLName</key>
<string>#{app_name}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>#{scheme}</string>
</array>
</dict>
"""
end <>
"</array>"
document_types =
"""
\n<key>CFBundleDocumentTypes</key>
<array>
""" <>
for type <- options[:document_types] || [], into: "" 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>"}
</array>
</dict>
"""
end <>
"</array>"
"""
<?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>CFBundleName</key>
<string>#{app_name}</string>
<key>CFBundleDisplayName</key>
<string>#{app_name}</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>#{url_schemes}#{document_types}
</dict>
</plist>
"""
end
defp create_logo(app_bundle_path, logo_source_path) do
logo_dest_path = Path.join([app_bundle_path, "Contents", "Resources", "AppIcon.icns"])
if Path.extname(logo_source_path) == ".icns" do
File.cp!(logo_source_path, logo_dest_path)
else
logo_dest_tmp_path = "tmp/AppIcon.iconset"
File.rm_rf!(logo_dest_tmp_path)
File.mkdir_p!(logo_dest_tmp_path)
sizes = for(i <- [16, 32, 64, 128], j <- [1, 2], do: {i, j}) ++ [{512, 1}]
for {size, scale} <- sizes do
suffix =
case scale do
1 -> ""
2 -> "@2x"
end
size = size * scale
out = "#{logo_dest_tmp_path}/icon_#{size}x#{size}#{suffix}.png"
cmd!("sips", ~w(-z #{size} #{size} #{logo_source_path} --out #{out}))
end
cmd!("iconutil", ~w(-c icns #{logo_dest_tmp_path} -o #{logo_dest_path}))
File.rm_rf!(logo_dest_tmp_path)
end
end
end

View file

@ -0,0 +1,13 @@
defmodule AppBuilder.Utils do
@moduledoc false
def cmd!(bin, args, opts \\ []) do
opts = Keyword.put_new(opts, :into, IO.stream())
{_, 0} = System.cmd(bin, args, opts)
end
def shell!(command, opts \\ []) do
opts = Keyword.put_new(opts, :into, IO.stream())
{_, 0} = System.shell(command, opts)
end
end

23
app_builder/mix.exs Normal file
View file

@ -0,0 +1,23 @@
defmodule AppBuilder.MixProject do
use Mix.Project
def project do
[
app: :app_builder,
version: "0.1.0",
elixir: "~> 1.13",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
def application do
[
extra_applications: [:logger]
]
end
defp deps do
[]
end
end

View file

@ -0,0 +1,3 @@
defmodule AppBuilderTest do
use ExUnit.Case, async: true
end

View file

@ -0,0 +1 @@
ExUnit.start()

View file

@ -10,24 +10,25 @@ defmodule Livebook.Application do
validate_hostname_resolution!()
set_cookie()
children = [
# Start the Telemetry supervisor
LivebookWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: Livebook.PubSub},
# Start the tracker server on this node
{Livebook.Tracker, pubsub_server: Livebook.PubSub},
# Start the supervisor dynamically managing sessions
{DynamicSupervisor, name: Livebook.SessionSupervisor, strategy: :one_for_one},
# Start the server responsible for associating files with sessions
Livebook.Session.FileGuard,
# Start the Node Pool for managing node names
Livebook.Runtime.NodePool,
# Start the unique task dependencies
Livebook.UniqueTask,
# Start the Endpoint (http/https)
LivebookWeb.Endpoint
]
children =
[
# Start the Telemetry supervisor
LivebookWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: Livebook.PubSub},
# Start the tracker server on this node
{Livebook.Tracker, pubsub_server: Livebook.PubSub},
# Start the supervisor dynamically managing sessions
{DynamicSupervisor, name: Livebook.SessionSupervisor, strategy: :one_for_one},
# Start the server responsible for associating files with sessions
Livebook.Session.FileGuard,
# Start the Node Pool for managing node names
Livebook.Runtime.NodePool,
# Start the unique task dependencies
Livebook.UniqueTask,
# Start the Endpoint (http/https)
LivebookWeb.Endpoint
] ++ app_specs()
opts = [strategy: :one_for_one, name: Livebook.Supervisor]
@ -157,4 +158,10 @@ defmodule Livebook.Application do
defp config_env_var?("LIVEBOOK_" <> _), do: true
defp config_env_var?("RELEASE_" <> _), do: true
defp config_env_var?(_), do: false
if Mix.target() == :app do
defp app_specs, do: [LivebookApp]
else
defp app_specs, do: []
end
end

View file

@ -304,6 +304,20 @@ defmodule Livebook.Utils do
"data:#{mime};base64,#{data}"
end
@doc """
Opens the given `url` in the browser.
"""
def browser_open(url) do
{cmd, args} =
case :os.type() do
{:win32, _} -> {"cmd", ["/c", "start", url]}
{:unix, :darwin} -> {"open", [url]}
{:unix, _} -> {"xdg-open", [url]}
end
System.cmd(cmd, args)
end
@doc """
Splits the given string at the last occurrence of `pattern`.

120
lib/livebook_app.ex Normal file
View file

@ -0,0 +1,120 @@
if Mix.target() == :app do
defmodule LivebookApp do
@moduledoc false
@behaviour :wx_object
# https://github.com/erlang/otp/blob/OTP-24.1.2/lib/wx/include/wx.hrl#L1314
@wx_id_exit 5006
def start_link(_) do
{:wx_ref, _, _, pid} = :wx_object.start_link(__MODULE__, [], [])
{:ok, pid}
end
def child_spec(init_arg) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [init_arg]},
restart: :transient
}
end
@impl true
def init(_) do
title = "Livebook"
wx = :wx.new()
frame = :wxFrame.new(wx, -1, title, size: {0, 0})
if macos?() do
fixup_macos_menubar(frame, title)
end
:wxFrame.show(frame)
:wxFrame.connect(frame, :command_menu_selected)
:wxFrame.connect(frame, :close_window, skip: true)
:wx.subscribe_events()
state = %{frame: frame}
Livebook.Utils.browser_open(LivebookWeb.Endpoint.access_url())
{frame, state}
end
@impl true
def handle_event({:wx, @wx_id_exit, _, _, _}, state) do
:init.stop()
{:stop, :normal, state}
end
@impl true
def handle_event({:wx, _, _, _, {:wxClose, :close_window}}, state) do
:init.stop()
{:stop, :normal, state}
end
# TODO: investigate "Universal Links" [1], that is, instead of livebook://foo, we have
# https://livebook.dev/foo, which means the link works with and without Livebook.app.
#
# [1] https://developer.apple.com/documentation/xcode/allowing-apps-and-websites-to-link-to-your-content
@impl true
def handle_info({:open_url, url}, state) do
'livebook://' ++ rest = url
import_livebook("https://#{rest}")
{:noreply, state}
end
@impl true
def handle_info({:open_file, path}, state) do
import_livebook("file://#{path}")
{:noreply, state}
end
# ignore other events
@impl true
def handle_info(_event, state) do
{:noreply, state}
end
defp import_livebook(url) do
%{
URI.parse(LivebookWeb.Endpoint.access_url())
| path: "/import"
}
|> append_query("url=#{URI.encode_www_form(url)}")
|> URI.to_string()
|> Livebook.Utils.browser_open()
end
defp fixup_macos_menubar(frame, title) do
menubar = :wxMenuBar.new()
:wxFrame.setMenuBar(frame, menubar)
# App Menu
menu = :wxMenuBar.oSXGetAppleMenu(menubar)
# Remove all items except for quit
for item <- :wxMenu.getMenuItems(menu) do
if :wxMenuItem.getId(item) == @wx_id_exit do
:wxMenuItem.setText(item, "Quit #{title}\tCtrl+Q")
else
:wxMenu.delete(menu, item)
end
end
end
defp macos?() do
:os.type() == {:unix, :darwin}
end
# TODO: On Elixir v1.14, use URI.append_query/2
defp append_query(%URI{query: query} = uri, query_to_add) when query in [nil, ""] do
%{uri | query: query_to_add}
end
defp append_query(%URI{} = uri, query) do
%{uri | query: uri.query <> "&" <> query}
end
end
end

View file

@ -118,13 +118,13 @@ defmodule LivebookCLI.Server do
defp open_from_options(base_url, opts) do
if opts[:open] do
browser_open(base_url)
Livebook.Utils.browser_open(base_url)
end
if opts[:open_new] do
base_url
|> append_path("/explore/notebooks/new")
|> browser_open()
|> Livebook.Utils.browser_open()
end
end
@ -213,17 +213,6 @@ defmodule LivebookCLI.Server do
defp opts_to_config([_opt | opts], config), do: opts_to_config(opts, config)
defp browser_open(url) do
{cmd, args} =
case :os.type() do
{:win32, _} -> {"cmd", ["/c", "start", url]}
{:unix, :darwin} -> {"open", [url]}
{:unix, _} -> {"xdg-open", [url]}
end
System.cmd(cmd, args)
end
defp append_path(url, path) do
url
|> URI.parse()

49
mix.exs
View file

@ -24,11 +24,16 @@ defmodule Livebook.MixProject do
def application do
[
mod: {Livebook.Application, []},
extra_applications: [:logger, :runtime_tools, :os_mon, :inets, :ssl, :xmerl],
extra_applications:
[:logger, :runtime_tools, :os_mon, :inets, :ssl, :xmerl] ++
extra_applications(Mix.target()),
env: Application.get_all_env(:livebook)
]
end
defp extra_applications(:app), do: [:wx]
defp extra_applications(_), do: []
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
@ -62,7 +67,8 @@ defmodule Livebook.MixProject do
{:aws_signature, "~> 0.2.0"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:floki, ">= 0.27.0", only: :test},
{:bypass, "~> 2.1", only: :test}
{:bypass, "~> 2.1", only: :test},
{:app_builder, path: "app_builder", targets: [:app]}
]
end
@ -103,10 +109,49 @@ defmodule Livebook.MixProject do
livebook: [
include_executables_for: [:unix],
include_erts: false
],
mac_app: [
include_executables_for: [:unix],
rel_templates_path: "rel/app",
steps: [:assemble, &build_mac_app/1]
],
mac_app_dmg: [
include_executables_for: [:unix],
rel_templates_path: "rel/app",
steps: [:assemble, &build_mac_app_dmg/1]
]
]
end
@app_options [
name: "Livebook",
logo_path: "static/images/logo.png",
url_schemes: ["livebook"],
document_types: [
%{name: "LiveMarkdown", role: "Editor", extensions: ["livemd"]}
]
]
defp build_mac_app(release) do
AppBuilder.build_mac_app(release, @app_options)
end
defp build_mac_app_dmg(release) 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")
]
] ++ @app_options
AppBuilder.build_mac_app_dmg(release, options)
end
def package do
[
licenses: ["Apache-2.0"],

1
rel/app/env.sh.bat Normal file
View file

@ -0,0 +1 @@
set RELEASE_MODE=interactive

1
rel/app/env.sh.eex Normal file
View file

@ -0,0 +1 @@
export RELEASE_MODE=interactive