Skip to main content
Glama
boot_script_builder.erl7.18 kB
%% Copyright (c) Meta Platforms, Inc. and affiliates. %% %% This source code is licensed under both the MIT license found in the %% LICENSE-MIT file in the root directory of this source tree and the Apache %% License, Version 2.0 found in the LICENSE-APACHE file in the root directory %% of this source tree. %% @format -module(boot_script_builder). -author("loscher@fb.com"). -moduledoc """ Build the release resource file, and boot scripts from a given spec file. The spec file format is defined in erlang_release.bzl usage: boot_script_builder.escript release_spec.term output_dir """. -export([main/1]). -define(EXITSUCCESS, 0). -define(EXITERROR, 1). -spec main([string()]) -> ok. main([ReleaseSpec, OutDir]) -> try do(ReleaseSpec, OutDir), erlang:halt(?EXITSUCCESS) catch Type:{abort, Reason} -> io:format(standard_error, "~s:~s~n", [Type, Reason]), erlang:halt(?EXITERROR) end; main(_) -> usage(). -spec usage() -> ok. usage() -> io:format("boot_script_builder.escript release_spec.term output_dir~n"). do(SpecFile, OutDir) -> {ok, SpecFileContents} = file:read_file(SpecFile, [raw]), #{ <<"apps">> := Apps, <<"lib_dir">> := LibDir, <<"name">> := RelNameBin, <<"version">> := RelVersionBin } = json:decode(SpecFileContents), OTPAppMapping = get_otp_apps_mapping(), LibDirWildcard = binary_to_list(filename:absname(filename:join([LibDir, "*", "ebin"]))), %% systools requires us to be in the same directory as the .rel file %% it also magically discovers the release root and makes all paths %% relative to it if the structure is OTP compliant ok = filelib:ensure_path(OutDir), ok = file:set_cwd(OutDir), RelName = binary_to_list(RelNameBin), RelVersion = binary_to_list(RelVersionBin), build_start_boot(Apps, LibDirWildcard, RelName, RelVersion, OTPAppMapping). build_start_boot( Apps, LibDirWildcard, RelName, RelVersion, OTPAppMapping ) -> %% OTP apps dependencies are not captures and we need to calculate %% them first {OTPApps, Others} = lists:partition( fun(#{<<"resolved">> := Resolved}) -> not Resolved end, Apps ), OTPAppDeps = get_otp_app_deps(OTPApps, OTPAppMapping), %% rel file content RelFileContent = { release, {RelName, RelVersion}, {erts, erlang:system_info(version)}, [ case StartType of <<"permanent">> -> {binary_to_atom(AppName), binary_to_list(AppVersion)}; <<"load">> -> {binary_to_atom(AppName), binary_to_list(AppVersion), load} end || #{ <<"name">> := AppName, <<"version">> := AppVersion, <<"type">> := StartType } <- Others ++ OTPAppDeps ] }, make_boot_script(RelName, RelFileContent, [ no_warn_sasl, no_dot_erlang, {script_name, "start"}, {path, [LibDirWildcard]} ]). make_boot_script(RelName, RelFileContent, Options) -> ok = write_file( io_lib:format("~s.rel", [RelName]), io_lib:format("~p.", [RelFileContent]) ), case systools:make_script(RelName, Options) of ok -> ok; Error -> error({abort, Error}) end. get_otp_apps_mapping() -> lists:foldl( fun(Path, Acc) -> case parse_app_path(Path) of skip -> Acc; [AppName, AppVersion] -> Acc#{ list_to_binary(AppName) => #{ version => list_to_binary(AppVersion), app_file => app_file_path(Path, AppName) } } end end, #{}, code:get_path() ). get_otp_app_deps(OTPApps, OTPAppMapping) -> InitialDeps = [ Dependency || #{<<"name">> := AppName} <- OTPApps, Dependency <- begin #{AppName := #{app_file := AppFile, version := AppVersion}} = OTPAppMapping, #{name := AppName, version := AppVersion, deps := Dependencies} = parse_app_file( AppFile ), Dependencies end ], InitialAcc = lists:foldl( fun(UnVersionedSpec = #{<<"name">> := AppName}, Acc) -> % get specific version from toolchain #{AppName := #{version := AppVersion}} = OTPAppMapping, % replace dynamic version with specific one VersionedSpec = UnVersionedSpec#{<<"version">> => AppVersion}, Acc#{binary_to_atom(AppName) => VersionedSpec} end, #{}, OTPApps ), get_otp_app_deps(InitialDeps, OTPAppMapping, InitialAcc). get_otp_app_deps([], _, Acc) -> maps:values(Acc); get_otp_app_deps([App | Rest], OTPAppMapping, Acc) -> AppName = atom_to_binary(App), #{AppName := #{app_file := AppFile, version := AppVersion}} = OTPAppMapping, #{name := AppName, version := AppVersion, deps := Dependencies} = parse_app_file(AppFile), Spec = #{ <<"name">> => AppName, <<"version">> => AppVersion, <<"type">> => <<"permanent">> }, FilteredDependencies = [Dep || Dep <- Dependencies, not is_map_key(Dep, Acc)], get_otp_app_deps(Rest ++ FilteredDependencies, OTPAppMapping, Acc#{App => Spec}). app_file_path(Path, Name) -> AppFile = unicode:characters_to_list(io_lib:format("~s.app", [Name])), filename:join(Path, AppFile). parse_app_path(Path) -> case string:prefix(Path, [code:lib_dir(), "/"]) of nomatch -> skip; RelPath -> case filename:split(RelPath) of [AppFolder, "ebin"] -> string:split(AppFolder, "-"); _ -> skip end end. parse_app_file(File) -> io:format("~s~n", [File]), {ok, [{application, App, Props}]} = file:consult(File), AppName = atom_to_binary(App), Version = list_to_binary(proplists:get_value(vsn, Props)), Dependencies = proplists:get_value(applications, Props, []) ++ proplists:get_value(included_applications, Props, []), #{ name => AppName, version => Version, deps => Dependencies }. -spec write_file(file:name_all() | iodata(), iolist()) -> string(). write_file(File, Data) -> case % We write in raw mode because this is a standalone escript, so we don't % need advanced file server features, and we gain performance by avoiding % calling through the file server file:open(File, [write, binary, raw]) of {ok, Handle} -> try % We use file:pwrite instead of file:write_file to work around % the latter needlessly flattening iolists (as returned by % json:encode/1, etc.) to a binary file:pwrite(Handle, 0, Data) after file:close(Handle) end, ok; {error, _} = Error -> Error end.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server