diff --git a/app_builder/examples/wx_demo/mix.exs b/app_builder/examples/wx_demo/mix.exs index 1647f3dcd..7560b2bfa 100644 --- a/app_builder/examples/wx_demo/mix.exs +++ b/app_builder/examples/wx_demo/mix.exs @@ -46,7 +46,6 @@ defmodule WxDemo.MixProject do ] ] ], - event_handler: WxDemo.Window, macos: [ build_dmg: macos_notarization != nil, notarization: macos_notarization diff --git a/app_builder/lib/app_builder.ex b/app_builder/lib/app_builder.ex index 4a246e618..77bdcac88 100644 --- a/app_builder/lib/app_builder.ex +++ b/app_builder/lib/app_builder.ex @@ -1,6 +1,6 @@ defmodule AppBuilder do def bundle(release) do - options = validate_options(release.options[:app]) + options = validate_options(release.options[:app] || []) case os() do :macos -> @@ -19,25 +19,28 @@ defmodule AppBuilder do end def init do + {:ok, _} = Registry.register(AppBuilder.Registry, "app_event_subscribers", []) + if input = System.get_env("APP_BUILDER_INPUT") do - __rpc__(self(), input) + __rpc__(input) end end - def __rpc__(event_handler) do - input = IO.read(:line) |> String.trim() - __rpc__(event_handler, input) + def __rpc__ do + IO.read(:line) + |> String.trim() + |> __rpc__() end - def __rpc__(event_handler, "open_app") do - send(event_handler, :open_app) + def __rpc__("open_app") do + dispatch(:open_app) end - def __rpc__(event_handler, "open_url:" <> url) do - send(event_handler, {:open_url, url}) + def __rpc__("open_url:" <> url) do + dispatch({:open_url, url}) end - def __rpc__(event_handler, "open_file:" <> path) do + def __rpc__("open_file:" <> path) do path = if os() == :windows do String.replace(path, "\\", "/") @@ -45,7 +48,13 @@ defmodule AppBuilder do path end - send(event_handler, {:open_file, path}) + dispatch({:open_file, path}) + end + + defp dispatch(message) do + Registry.dispatch(AppBuilder.Registry, "app_event_subscribers", fn entries -> + for {pid, _} <- entries, do: send(pid, message) + end) end defp validate_options(options) do @@ -55,7 +64,6 @@ defmodule AppBuilder do all: [ :name, :icon_path, - :event_handler, url_schemes: [], document_types: [], additional_paths: [] @@ -83,15 +91,20 @@ defmodule AppBuilder do windows: [] } - options = validate_options(options, root_allowed_options, os) - - Keyword.update!(options, :document_types, fn document_types -> + options + |> validate_options(root_allowed_options, os) + |> Keyword.put_new_lazy(:name, &default_name/0) + |> Keyword.update!(:document_types, fn document_types -> Enum.map(document_types, fn options -> validate_options(options, document_type_allowed_options, os) end) end) end + defp default_name do + Mix.Project.config()[:app] |> to_string |> Macro.camelize() + end + defp validate_options(options, allowed, os) do {macos_options, options} = Keyword.pop(options, :macos, []) {windows_options, options} = Keyword.pop(options, :windows, []) diff --git a/app_builder/lib/app_builder/application.ex b/app_builder/lib/app_builder/application.ex new file mode 100644 index 000000000..ddcd0580a --- /dev/null +++ b/app_builder/lib/app_builder/application.ex @@ -0,0 +1,13 @@ +defmodule AppBuilder.Application do + @moduledoc false + + use Application + + def start(_type, _args) do + children = [ + {Registry, keys: :duplicate, name: AppBuilder.Registry} + ] + + Supervisor.start_link(children, strategy: :one_for_one, name: AppBuilder.Supervisor) + end +end diff --git a/app_builder/lib/templates/macos/Launcher.swift.eex b/app_builder/lib/templates/macos/Launcher.swift.eex index 97abc6d60..90cde8df6 100644 --- a/app_builder/lib/templates/macos/Launcher.swift.eex +++ b/app_builder/lib/templates/macos/Launcher.swift.eex @@ -1,13 +1,8 @@ <% -event_handler = @app_options |> Keyword.fetch!(:event_handler) |> inspect() +additional_paths = Enum.map_join(@app_options[:additional_paths], ":", &"\\(resourcePath)/#{&1}") -additional_paths = - ["rel/erts-#{@release.erts_version}/bin"] ++ @app_options[:additional_paths] - |> Enum.map_join(":", &"\\(resourcePath)/#{&1}") - -%> -import Cocoa +%>import Cocoa class AppDelegate: NSObject, NSApplicationDelegate { var releaseTask: Process! @@ -45,7 +40,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } } - } func startRelease(_ input : String) -> Process { @@ -78,7 +72,7 @@ func rpc(_ event: String) { let task = buildReleaseTask() task.standardInput = input input.fileHandleForWriting.write("\(event)\n".data(using: .utf8)!) - task.arguments = ["rpc", "AppBuilder.__rpc__(<%= event_handler %>)"] + task.arguments = ["rpc", "AppBuilder.__rpc__()"] try! task.run() task.waitUntilExit() @@ -88,17 +82,17 @@ func rpc(_ event: String) { } func buildReleaseTask() -> Process { - let releaseScriptPath = Bundle.main.path(forResource: "rel/bin/<%= @release.name %>", ofType: "")! + let task = Process() + task.launchPath = Bundle.main.path(forResource: "rel/bin/<%= @release.name %>", ofType: "")! + task.environment = ProcessInfo.processInfo.environment + +<%= if additional_paths != "" do %> let resourcePath = Bundle.main.resourcePath ?? "" let additionalPaths = "<%= additional_paths %>" + let path = task.environment!["PATH"] ?? "" + task.environment!["PATH"] = "\(additionalPaths):\(path)" +<% end %> - var environment = ProcessInfo.processInfo.environment - let path = environment["PATH"] ?? "" - environment["PATH"] = "\(additionalPaths):\(path)" - - let task = Process() - task.environment = environment - task.launchPath = releaseScriptPath task.standardOutput = logFile task.standardError = logFile return task diff --git a/app_builder/lib/templates/windows/Installer.nsi.eex b/app_builder/lib/templates/windows/Installer.nsi.eex index 30375a471..956dd727a 100644 --- a/app_builder/lib/templates/windows/Installer.nsi.eex +++ b/app_builder/lib/templates/windows/Installer.nsi.eex @@ -7,6 +7,7 @@ app_name = Keyword.fetch!(@app_options, :name) ;General Name "<%= app_name %>" +ManifestDPIAware true OutFile "<%= app_name %>Install.exe" Unicode True InstallDir "$LOCALAPPDATA\<%= app_name %>" diff --git a/app_builder/lib/templates/windows/Launcher.vbs.eex b/app_builder/lib/templates/windows/Launcher.vbs.eex index a11bb0def..0d61702fe 100644 --- a/app_builder/lib/templates/windows/Launcher.vbs.eex +++ b/app_builder/lib/templates/windows/Launcher.vbs.eex @@ -1,10 +1,9 @@ <% -event_handler = @app_options |> Keyword.fetch!(:event_handler) |> inspect() - additional_paths = - ["rel/erts-#{@release.erts_version}/bin"] ++ @app_options[:additional_paths] - |> Enum.map(&("root & \"" <> String.replace(&1, "/", "\\") <> ";\" & ")) + for path <- Keyword.fetch!(@app_options, :additional_paths), into: "" do + "root & \"" <> String.replace(path, "/", "\\") <> ";\" & " + end %> @@ -19,10 +18,12 @@ End If Set shell = CreateObject("WScript.Shell") Set env = shell.Environment("Process") +<%= if additional_paths != "" do %> env("PATH") = <%= additional_paths %>env("PATH") +<% end %> ' try release rpc, if release is down, this will fail but that's ok. -cmd = "echo " & input & " | """ & script & """ rpc ""AppBuilder.__rpc__(<%= event_handler %>)""" +cmd = "echo " & input & " | """ & script & """ rpc ""AppBuilder.__rpc__()""" status = shell.Run("cmd /c " & cmd, 0, true) ' try release start, if release is up, this will fail but that's ok. diff --git a/app_builder/mix.exs b/app_builder/mix.exs index 4e6ab82b9..d00946313 100644 --- a/app_builder/mix.exs +++ b/app_builder/mix.exs @@ -20,6 +20,7 @@ defmodule AppBuilder.MixProject do def application do [ + mod: {AppBuilder.Application, []}, extra_applications: [:logger, :eex, :inets, :ssl, :crypto] ] end diff --git a/mix.exs b/mix.exs index 77cfa3f1b..9bf2b870c 100644 --- a/mix.exs +++ b/mix.exs @@ -6,6 +6,7 @@ defmodule Livebook.MixProject do @description "Interactive and collaborative code notebooks - made with Phoenix LiveView" @app_elixir_version "1.13.4" + @app_rebar3_version "3.19.0" def project do [ @@ -160,8 +161,8 @@ defmodule Livebook.MixProject do ] ] ], - event_handler: LivebookApp, additional_paths: [ + "rel/erts-#{:erlang.system_info(:version)}/bin", "rel/vendor/elixir/bin" ], macos: [ @@ -201,5 +202,7 @@ defmodule Livebook.MixProject do release |> Standalone.copy_otp() |> Standalone.copy_elixir(@app_elixir_version) + |> Standalone.copy_hex() + |> Standalone.copy_rebar3(@app_rebar3_version) end end diff --git a/rel/app/env.bat.eex b/rel/app/env.bat.eex index 3c77f48ee..77eeec0ff 100644 --- a/rel/app/env.bat.eex +++ b/rel/app/env.bat.eex @@ -1,5 +1,7 @@ set RELEASE_NODE=livebook_app set RELEASE_MODE=interactive +set MIX_ARCHIVES=!RELEASE_ROOT!\vendor\archives +set MIX_REBAR3=!RELEASE_ROOT!\vendor\rebar3 set LIVEBOOK_SHUTDOWN_ENABLED=true set cookie_path="!RELEASE_ROOT!\releases\COOKIE" diff --git a/rel/app/env.sh.eex b/rel/app/env.sh.eex index f60ad526e..b84bdf9bb 100644 --- a/rel/app/env.sh.eex +++ b/rel/app/env.sh.eex @@ -1,7 +1,9 @@ export RELEASE_NODE=livebook_app export RELEASE_MODE=interactive -export LIVEBOOK_SHUTDOWN_ENABLED=true +export MIX_ARCHIVES="${RELEASE_ROOT}/vendor/archives" +export MIX_REBAR3="${RELEASE_ROOT}/vendor/rebar3" export WX_MACOS_NON_GUI_APP=1 +export LIVEBOOK_SHUTDOWN_ENABLED=true cookie_path="${RELEASE_ROOT}/releases/COOKIE" if [ ! -f $cookie_path ]; then diff --git a/rel/app/standalone.exs b/rel/app/standalone.exs index e5373e742..a272d61f0 100644 --- a/rel/app/standalone.exs +++ b/rel/app/standalone.exs @@ -7,7 +7,9 @@ defmodule Standalone do """ @spec copy_otp(Mix.Release.t()) :: Mix.Release.t() def copy_otp(release) do - {erts_source, otp_bin_dir, otp_lib_dir} = otp_dirs() + erts_source = Path.join(:code.root_dir(), "erts-#{release.erts_version}") + otp_bin_dir = Path.join(:code.root_dir(), "bin") + otp_lib_dir = :code.lib_dir() # 1. copy erts/bin release_erts_bin_dir = Path.join(release.path, "erts-#{release.erts_version}/bin") @@ -39,6 +41,10 @@ defmodule Standalone do release_lib_dir = Path.join(release.path, "lib") cp_r!(otp_lib_dir, release_lib_dir) + for dir <- Path.wildcard("#{release_lib_dir}/*/doc/{xml,html,pdf}") do + File.rm_rf!(dir) + end + # 3. copy boot files release_bin_dir = Path.join(release.path, "bin") @@ -58,7 +64,7 @@ defmodule Standalone do download_elixir_at_destination(standalone_destination, elixir_version) filenames = - case os() do + case AppBuilder.os() do :macos -> ["elixir", "elixirc", "mix", "iex"] @@ -83,12 +89,40 @@ defmodule Standalone do :zip.unzip(String.to_charlist(path), cwd: destination) end - defp otp_dirs do - version = :erlang.system_info(:version) - root_dir = :code.root_dir() + @doc """ + Copies Hex into the release. + """ + @spec copy_hex(Mix.Release.t()) :: Mix.Release.t() + def copy_hex(release) do + release_archives_dir = Path.join(release.path, "vendor/archives") + File.mkdir_p!(release_archives_dir) - {:filename.join(root_dir, 'erts-#{version}'), :filename.join(root_dir, 'bin'), - :code.lib_dir()} + hex_version = Keyword.fetch!(Application.spec(:hex), :vsn) + source_hex_path = Path.join(Mix.path_for(:archives), "hex-#{hex_version}") + release_hex_path = Path.join(release_archives_dir, "hex-#{hex_version}") + cp_r!(source_hex_path, release_hex_path) + + release + end + + @doc """ + Copies Rebar3 into the release. + """ + @spec copy_rebar3(Mix.Release.t(), version :: String.t()) :: Mix.Release.t() + def copy_rebar3(release, version) do + url = "https://github.com/erlang/rebar3/releases/download/#{version}/rebar3" + path = Path.join(System.tmp_dir!(), "rebar3_#{version}") + + unless File.exists?(path) do + binary = fetch_body!(url) + File.write!(path, binary, [:binary]) + end + + destination = Path.join(release.path, "vendor/rebar3") + File.cp!(path, destination) + make_executable(destination) + + release end defp fetch_body!(url) do @@ -105,13 +139,6 @@ defmodule Standalone do defp make_executable(path), do: File.chmod!(path, 0o755) - defp os() do - case :os.type() do - {:unix, :darwin} -> :macos - {:win32, _} -> :windows - end - end - defp cp_r!(source, destination) do File.cp_r!(source, destination, fn _, _ -> false end) end