%% SPDX-License-Identifier: Apache-2.0 %% SPDX-FileCopyrightText: 2025 Stritzinger GmbH -module(cassone_prv). -export([init/1, do/1, format_error/1]). -define(PROVIDER, cassone). -define(DEPS, [compile]). %% =================================================================== %% Public API %% =================================================================== -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. init(State) -> Provider = providers:create([ {name, ?PROVIDER}, % The 'user friendly' name of the task {module, ?MODULE}, % The module implementation of the task {bare, true}, % The task can be run by the user, always true {deps, ?DEPS}, % The list of dependencies {example, "rebar3 cassone"}, % How to use the plugin {opts, []}, % list of options understood by the plugin {short_desc, "A rebar plugin"}, {desc, "A rebar plugin"} ]), {ok, rebar_state:add_provider(State, Provider)}. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. do(State) -> [AppInfo] = rebar_state:project_apps(State), Version = rebar_app_info:original_vsn(AppInfo), EscriptDir = filename:join(rebar_dir:base_dir(State), "bin"), CassoneDir = filename:join(rebar_dir:base_dir(State), "cassone"), rebar_utils:sh(io_lib:format("rm -rf ~s", [CassoneDir]), []), ok = filelib:ensure_path(EscriptDir), ok = filelib:ensure_path(CassoneDir), Config = rebar_state:get(State, cassone, []), Mode = get_option(mode, Config, release), Targets = get_option(targets, Config, [current_machine]), {HostArch, HostOS} = system_architecture(), AzdoraPath = cassone_piadina:fetch_azdora(HostOS, HostArch), Opts = #{ version => Version, mode => Mode, escript_dir => EscriptDir, cassone_dir => CassoneDir, azdora_path => AzdoraPath }, {Opts2, State1} = release(Mode, Opts, State), [begin cleanup_working_dir(Opts2), assemble_target(TGT, Opts2) end || TGT <- Targets], {ok, State1}. -spec format_error(any()) -> iolist(). format_error(Reason) -> io_lib:format("~p", [Reason]). %% Internal functions ---------------------------------------------------------- release(escript, Opts, State) -> {ok, S} = rebar_prv_escriptize:do(State), EscriptName = rebar_state:get(State, escript_name), {Opts#{escript_name => atom_to_list(EscriptName)}, S}; release(release, _, _) -> rebar_api:abort("cassone: release mode is not supported yet", []). get_option(Option, Config, Default) -> case lists:keyfind(Option, 1, Config) of {Option, Value} -> Value; false -> Default end. cleanup_working_dir(#{cassone_dir := CassoneDir, escript_name := EscriptName}) -> rebar_api:info("cassone: cleaning up working directory", []), ReleaseDir = filename:join([CassoneDir, "working", EscriptName]), rebar_utils:sh(io_lib:format("rm -rf ~s", [ReleaseDir]), []), ok. assemble_target({OS, Arch}, #{azdora_path := AzdoraPath} = Options) -> OtpVersion = otp_version(), TargetOTPDir = cassone_erts:fetch(OtpVersion, OS, Arch), run_otp_install(TargetOTPDir), PiadinaPath = cassone_piadina:fetch_piadina(OS, Arch), MuslcRuntime = cassone_musl:fetch(OS,Arch), rebar_utils:sh("chmod +x " ++ MuslcRuntime, []), rebar_utils:sh("chmod +x " ++ AzdoraPath, []), rebar_utils:sh("chmod +x " ++ PiadinaPath, []), copy_released_files(Options), Options2 = copy_target_otp(TargetOTPDir, Options), copy_musl_runtime(MuslcRuntime, Options2), MuslcRuntimeName = filename:basename(MuslcRuntime), Options3 = Options2#{target_arch => Arch, target_os => OS}, cook(AzdoraPath, PiadinaPath, MuslcRuntimeName, Options3); assemble_target(Target, _) -> rebar_api:warning("cassone: unsupported target: ~p", [Target]). run_otp_install(TargetOTPDir) -> [InstallScript | _] = filelib:wildcard("*/Install", TargetOTPDir), FullInstallScriptName = filename:join([TargetOTPDir, InstallScript]), InstallDir = filename:dirname(FullInstallScriptName), rebar_utils:sh(FullInstallScriptName ++ " -minimal " ++ InstallDir, []). copy_released_files(#{ mode := escript, cassone_dir := CassoneDir, escript_dir := EscriptDir, escript_name := EscriptName }) -> rebar_api:info("cassone: copying released files", []), RelEscriptLocation = filename:join([EscriptDir, EscriptName]), RelEscriptDst = filename:join([CassoneDir, EscriptName, "bin", EscriptName]), filelib:ensure_dir(RelEscriptDst), cp_cmd(RelEscriptLocation, RelEscriptDst). copy_target_otp(TargetOTPDir, #{ mode := escript, cassone_dir := CassoneDir, escript_name := EscriptName } = Options) -> PayloadLocation = filename:join([CassoneDir, EscriptName]), rebar_api:info("cassone: copying erts, libs and binaries from ~p to ~p", [TargetOTPDir, PayloadLocation]), [ErtsFolder | _] = filelib:wildcard("*/erts-*", TargetOTPDir), [_, ErtsVersion] = string:split(ErtsFolder, "-"), ErtsFullPath = filename:join([TargetOTPDir, ErtsFolder]), cp_cmd(ErtsFullPath, PayloadLocation), [LibFolder | _] = filelib:wildcard("*/lib", TargetOTPDir), LibDir = filename:join([TargetOTPDir, LibFolder]), cp_cmd(LibDir, PayloadLocation), [BinFolder | _] = filelib:wildcard("*/bin", TargetOTPDir), BinDir = filename:join([TargetOTPDir, BinFolder]), cp_cmd(BinDir, PayloadLocation), % We replace epmd symlink with the real file to avoid an error from tar+gzip. rebar_api:info("cassone: removing epmd symlink", []), rebar_utils:sh("rm " ++ filename:join([PayloadLocation, "bin", "epmd"]), []), Options#{erts_version => ErtsVersion}. copy_musl_runtime(MuslcRuntime, #{ mode := escript, cassone_dir := CassoneDir, escript_name := EscriptName }) -> rebar_api:info("cassone: copying musl runtime from ~p to ~p", [MuslcRuntime, CassoneDir]), cp_cmd(MuslcRuntime, filename:join([CassoneDir, EscriptName])). cp_cmd(Src, Dst) -> rebar_api:info("cassone: copying ~p to ~p", [Src, Dst]), case filelib:is_dir(Src) of true -> Flags = "-r"; false -> Flags = "" end, rebar_utils:sh(io_lib:format("cp ~s ~s ~s", [Flags, Src, Dst]), []). otp_version() -> Root = code:root_dir(), Major = erlang:system_info(otp_release), {ok, Version} = file:read_file(filename:join([Root, "releases", Major, "OTP_VERSION"])), string:trim(binary_to_list(Version)). -spec system_architecture() -> cassone:arch(). system_architecture() -> [Arch, _, OS | _] = string:split(erlang:system_info(system_architecture), "-", all), {list_to_existing_atom(Arch), os_to_atom(OS)}. os_to_atom("linux") -> linux; os_to_atom(["darwin", _]) -> macos. cook(AzdoraPath, PiadinaPath, MuslcRuntimeName, #{mode := escript} = Options) -> #{cassone_dir := CassoneDir, escript_name := EscriptName, version := Version, erts_version := ErtsVersion } = Options, MUSL = filename:join(["{PAYLOAD_ROOT}", MuslcRuntimeName]), EntryArgs = filename:join(["{PAYLOAD_ROOT}", "bin", EscriptName]), % TODO: do not hardcode paths containing erts and apps versions Command = AzdoraPath ++ " " "--launcher " ++ PiadinaPath ++ " " "--payload " ++ filename:join([CassoneDir, EscriptName]) ++ " " "--output " ++ output_bin_name(Options) ++ " " "--meta APP_NAME=" ++ EscriptName ++ " " "--meta APP_VER=" ++ Version ++ " " "--meta ENTRY_POINT=bin/escript " "--meta ENTRY_ARGS[]=" ++ EntryArgs ++ " " "--meta ENV.ERL_ROOTDIR=\"{PAYLOAD_ROOT}\" " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/escript:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=lib/erl_interface-5.6.1/bin/erl_call:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/run_erl:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/yielding_c_fun:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/erlc:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/epmd:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/dialyzer:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/erl_call:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/inet_gethost:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/beam.smp:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/erlexec:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/to_erl:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/erl_child_setup:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/heart:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/dyn_erl:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/ct_run:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/typer:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=erts-" ++ ErtsVersion ++ "/bin/escript:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=bin/run_erl:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=bin/erlc:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=bin/dialyzer:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=bin/erl_call:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=bin/to_erl:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=bin/ct_run:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=bin/typer:" ++ MUSL ++ " " "--meta PATCHELF_SET_INTERPRETER[]=bin/escript:" ++ MUSL ++ " ", rebar_utils:sh(Command, []). output_bin_name(Options) -> #{target_os := OS, target_arch := Arch, escript_name := EscriptName} = Options, EscriptName ++ "-" ++ atom_to_list(OS) ++ "-" ++ arch_to_string(Arch) ++ ".bin". arch_to_string(x86_64) -> "amd64"; arch_to_string(aarch64) -> "arm64".