From 829954b563d15053abee870207b3cdaafc085bbe Mon Sep 17 00:00:00 2001 From: Sebastien Merle Date: Fri, 30 Aug 2024 16:52:57 +0200 Subject: [PATCH] Add firmware command --- CHANGELOG.md | 1 + rebar.config | 3 +- rebar.lock | 6 + src/grisp_tools.erl | 3 + src/grisp_tools_firmware.erl | 401 +++++++++++++++++++++++++++++++++++ 5 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 src/grisp_tools_firmware.erl diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dba5e7..c3c2665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to receive the last know state before the exception was raised. The cleanup actions will **NOT** be called if the actions do not raise any exception. [#25](https://github.com/grisp/grisp_tools/pull/25) +- Add a firmware command to generate GRiSP 2 binary firmwares: [#26](https://github.com/grisp/grisp_tools/pull/26) ### Changed diff --git a/rebar.config b/rebar.config index 7eb9788..93fc67a 100644 --- a/rebar.config +++ b/rebar.config @@ -2,5 +2,6 @@ {deps, [ {mapz, "~> 2.2"}, bbmustache, - hackney + hackney, + edifa ]}. diff --git a/rebar.lock b/rebar.lock index 50fa3a7..4d64314 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,6 +1,8 @@ {"1.2.0", [{<<"bbmustache">>,{pkg,<<"bbmustache">>,<<"1.12.2">>},0}, {<<"certifi">>,{pkg,<<"certifi">>,<<"2.9.0">>},1}, + {<<"edifa">>,{pkg,<<"edifa">>,<<"1.0.0">>},0}, + {<<"erlexec">>,{pkg,<<"erlexec">>,<<"2.0.7">>},1}, {<<"hackney">>,{pkg,<<"hackney">>,<<"1.18.1">>},0}, {<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},1}, {<<"mapz">>,{pkg,<<"mapz">>,<<"2.2.0">>},0}, @@ -13,6 +15,8 @@ {pkg_hash,[ {<<"bbmustache">>, <<"0CABDCE0DB9FE6D3318131174B9F2B351328A4C0AFBEB3E6E99BB0E02E9B621D">>}, {<<"certifi">>, <<"6F2A475689DD47F19FB74334859D460A2DC4E3252A3324BD2111B8F0429E7E21">>}, + {<<"edifa">>, <<"0F1A01A0C79B7135F334B3FCEEB624F0574C5ED3E4554B06C8664AADA6A339C8">>}, + {<<"erlexec">>, <<"76D0BC7487929741B5BB9F74DA2AF5DAF1492134733CF9A05C7AAA278B6934C5">>}, {<<"hackney">>, <<"F48BF88F521F2A229FC7BAE88CF4F85ADC9CD9BCF23B5DC8EB6A1788C662C4F6">>}, {<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>}, {<<"mapz">>, <<"81EE5AD249BBC9427DAA6C3EE5166F88A448C5B0B2F5BBEE0495266308AFF2BA">>}, @@ -24,6 +28,8 @@ {pkg_hash_ext,[ {<<"bbmustache">>, <<"688B33A4D5CC2D51F575ADF0B3683FC40A38314A2F150906EDCFC77F5B577B3B">>}, {<<"certifi">>, <<"266DA46BDB06D6C6D35FDE799BCB28D36D985D424AD7C08B5BB48F5B5CDD4641">>}, + {<<"edifa">>, <<"A1E010561E7D236A24C668D95626BE2BFE082ED0331CE1E6798BE0CD43F59A7B">>}, + {<<"erlexec">>, <<"AF2DD940BB8E32F5AA40A65CB455DCAA18F5334FD3507E9BFD14A021E9630897">>}, {<<"hackney">>, <<"A4ECDAFF44297E9B5894AE499E9A070EA1888C84AFDD1FD9B7B2BC384950128E">>}, {<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>}, {<<"mapz">>, <<"861A878A86AB7E5897A9B57B3CA0B188FB53CE2935B153872C61F6641E709AA8">>}, diff --git a/src/grisp_tools.erl b/src/grisp_tools.erl index fbd4c2b..b38b0b3 100644 --- a/src/grisp_tools.erl +++ b/src/grisp_tools.erl @@ -8,6 +8,7 @@ -export([list_packages/1]). -export([report/1]). -export([configure/1]). +-export([firmware/1]). %--- API ----------------------------------------------------------------------- @@ -30,3 +31,5 @@ list_packages(Opts) -> grisp_tools_package:list(Opts). report(Opts) -> grisp_tools_report:run(Opts). configure(Opts) -> grisp_tools_configure:run(Opts). + +firmware(Opts) -> grisp_tools_firmware:run(Opts). diff --git a/src/grisp_tools_firmware.erl b/src/grisp_tools_firmware.erl new file mode 100644 index 0000000..0fc8414 --- /dev/null +++ b/src/grisp_tools_firmware.erl @@ -0,0 +1,401 @@ +-module(grisp_tools_firmware). + +-include_lib("kernel/include/file.hrl"). + +% API +-export([run/1]). + +-import(grisp_tools_util, [event/2]). +-import(grisp_tools_util, [shell/2]). +-import(grisp_tools_util, [shell/3]). + + +%--- MACROS -------------------------------------------------------------------- + +-define(MiB, * 1024 * 1024). +-define(DOCKER_TOOLCHAIN_ROOT, "/grisp2-rtems-toolchain/rtems/5"). +-define(GRISP2_BOOTLOADER_FILENAMES, [ + "foobar.img", + "barebox-phytec-phycore-imx6ull-emmc-512mb.img", + "barebox-phytec-phycore-imx6ul-emmc-512mb.img" +]). +-define(GRISP2_RESERVED_SIZE, (4?MiB)). +-define(GRISP2_SYSTEM_SIZE, (256?MiB)). +-define(GRISP2_IMAGE_SIZE, (?GRISP2_RESERVED_SIZE + 2 * ?GRISP2_SYSTEM_SIZE)). +-define(GRISP2_PARTITIONS, [ + #{type => fat32, size => ?GRISP2_SYSTEM_SIZE, + start => ?GRISP2_RESERVED_SIZE}, + #{type => fat32, size => ?GRISP2_SYSTEM_SIZE} +]). +-define(GRISP2_FAT_TYPE, 32). +-define(GRISP2_FAT_CLUSTER_SIZE, 4). + +%--- API ----------------------------------------------------------------------- + +run(State) -> + grisp_tools_util:weave(State, [ + fun grisp_tools_step:config/1, + {firmware, [ + fun prepare/1, + fun build_firmware/1 + ]} + ]). + +%--- Tasks --------------------------------------------------------------------- + +prepare(State) -> + Force = maps:get(force, State, false), + State2 = State#{force => Force}, + grisp_tools_util:weave(State2, [ + fun validate_platform/1, + fun validate_temp_dir/1, + fun validate_bundle/1, + fun validate_system/1, + fun validate_image/1, + fun validate_boot/1, + fun validate_toolchain/1 + ]). + +validate_platform(State) -> + case maps:find(platform, State) of + {ok, grisp2} -> State; + {ok, Platform} -> + event(State, [{error, unsupported_platform, Platform}]); + _ -> + event(State, [{error, missing_parameter, platform}]) + end. + +validate_temp_dir(State) -> + case maps:find(temp_dir, State) of + {ok, TempDir} when TempDir =/= undefined -> + case filelib:is_dir(TempDir) of + true -> State; + false -> event(State, [{error, directory_not_found, TempDir}]) + end; + _ -> + {{ok, Output}, State2} = shell(State, "mktemp -d"), + TempDir = string:trim(Output), + case filelib:is_dir(TempDir) of + true -> + State2#{temp_dir => TempDir, + cleanup_temp_dir => true}; + false -> + event(State2, [{error, directory_not_found, TempDir}]) + end + end. + +validate_bundle(State) -> + case maps:find(bundle, State) of + {ok, BundlePath} when BundlePath =/= undefined -> + case filelib:is_file(BundlePath) of + true -> State; + false -> + event(State, [{error, bundle_not_found, BundlePath}]) + end; + _ -> + event(State, [{error, missing_parameter, bundle}]) + end. + +validate_system(State) -> + case maps:find(system, State) of + error -> State#{system => undefined}; + {ok, undefined} -> State; + {ok, #{target := TargetPath} = Opts} -> + Compress = maps:get(compress, Opts, true), + State2 = prepare_output_file(State, TargetPath), + State2#{system => Opts#{compress => Compress}}; + {ok, Opts} -> + event(State, [{error, invalid_system_options, Opts}]) + end. + +validate_image(State) -> + case maps:find(image, State) of + error -> State#{image => undefined}; + {ok, undefined} -> State; + {ok, #{target := TargetPath} = Opts} -> + Compress = maps:get(compress, Opts, true), + Truncate = maps:get(truncate, Opts, true), + State2 = prepare_output_file(State, TargetPath), + Opts2 = Opts#{compress => Compress, truncate => Truncate}, + State2#{image => Opts2}; + {ok, Opts} -> + event(State, [{error, invalid_image_options, Opts}]) + end. + +validate_boot(State) -> + case maps:find(boot, State) of + error -> State#{boot => undefined}; + {ok, undefined} -> State; + {ok, #{target := TargetPath} = Opts} -> + Compress = maps:get(compress, Opts, true), + State2 = prepare_output_file(State, TargetPath), + State2#{boot => Opts#{compress => Compress}}; + {ok, Opts} -> + event(State, [{error, invalid_boot_options, Opts}]) + end. + +validate_toolchain(State = #{image := ImageSpec, boot := BootSpec}) + when ImageSpec =/= undefined; BootSpec =/= undefined -> + Toolchain = maps:get(toolchain, State, undefined), + {SelectedBootloader, State2} = select_bootloader(State, Toolchain), + case State2#{bootloader => SelectedBootloader} of + #{bootloader := undefined, image := ImageSpec, boot := BootSpec} + when ImageSpec =/= undefined; BootSpec =/= undefined -> + event(State, [{error, toolchain_required}]); + #{bootloader := undefined} = State3 -> + State3; + #{bootloader := BootloaderPath} = State3 -> + event(State3, [{bootloader, filename:basename(BootloaderPath)}]) + end; +validate_toolchain(State) -> + State#{bootloader => undefined}. + +build_firmware(State) -> + grisp_tools_util:weave(State, [ + fun create_image/1 + ] + ++ if_key_defined(State, bootloader, [fun copy_bootloader/1], []) + ++ [ + fun create_partitions/1, + fun format_system/1, + fun deploy_bundle/1 + ] + ++ if_key_defined(State, system, [fun extract_system/1], []) + ++ if_key_defined(State, image, [fun extract_image/1], []) + ++ if_key_defined(State, boot, [fun extract_boot/1], []) + ++ [ + fun close_image/1, + fun cleanup/1 + ], [ + fun close_image/1, + fun cleanup/1 + ]). + +create_image(State = #{temp_dir := TempDir}) -> + Opts = edifa_opts(State, #{temp_dir => TempDir}), + ImageFile = filename:join(TempDir, "emmc.img"), + case edifa:create(ImageFile, ?GRISP2_IMAGE_SIZE, Opts) of + {ok, Pid, State2} -> + State2#{edifa_pid => Pid}; + {error, Reason, State2} -> + event(State2, [{error, Reason}]) + end. + +copy_bootloader(State = #{edifa_pid := Pid, bootloader := BootFile}) -> + Opts = edifa_opts(State, #{count => ?GRISP2_RESERVED_SIZE}), + case edifa:write(Pid, BootFile, Opts) of + {ok, State2} -> State2; + {error, Reason, State2} -> + event(State2, [{error, Reason}]) + end. + +create_partitions(State = #{edifa_pid := Pid}) -> + Opts = edifa_opts(State), + case edifa:partition(Pid, mbr, ?GRISP2_PARTITIONS, Opts) of + {ok, [_, _] = Partitions, State2} -> + State2#{partitions => Partitions}; + {error, Reason, State2} -> + event(State2, [{error, Reason}]) + end. + +format_system(State = #{partitions := [PartId1, PartId2]}) -> + format_system(format_system(State, PartId1, "SYS"), PartId2, "SYS"). + +deploy_bundle(State = #{partitions := [PartId1, PartId2]}) -> + deploy_bundle(deploy_bundle(State, PartId1), PartId2). + +extract_system(State = #{edifa_pid := Pid, partitions := [PartId, _], + system := #{target := Path, + compress := Compress}}) -> + Opts = edifa_opts(State, #{ + compressed => Compress, + timeout => 10000 + }), + case edifa:extract(Pid, PartId, PartId, Path, Opts) of + {error, Reason, State2} -> + event(State2, [{error, Reason}]); + {ok, State2} -> + event(State2, [{extracted, Path}]) + end. + + +extract_image(State = #{edifa_pid := Pid, partitions := [PartId1, PartId2], + image := #{target := Path, + truncate := Truncated, + compress := Compress}}) -> + Opts = edifa_opts(State, #{ + compressed => Compress, + timeout => 20000 + }), + PartId = case Truncated of + true -> PartId1; + false -> PartId2 + end, + case edifa:extract(Pid, reserved, PartId, Path, Opts) of + {error, Reason, State2} -> + event(State2, [{error, Reason}]); + {ok, State2} -> + event(State2, [{extracted, Path}]) + end. + +extract_boot(State0 = #{edifa_pid := Pid, + boot := #{target := Path, + compress := Compress}}) -> + State = event(State0, [start_extracting_boot]), + Opts = edifa_opts(State, #{compressed => Compress}), + case edifa:extract(Pid, reserved, reserved, Path, Opts) of + {error, Reason, State2} -> + event(State2, [{error, Reason}]); + {ok, State2} -> + event(State2, [{extracted, Path}]) + end. + +close_image(State = #{edifa_pid := Pid}) -> + case edifa:close(Pid, edifa_opts(State)) of + {ok, State2} -> maps:remove(edifa_pid, State2); + {error, Reason, State2} -> + event(State2, [{error, Reason}]) + end. + +cleanup(State) -> + cleanup_temp_dir(cleanup_image(State)). + +cleanup_image(State = #{edifa_pid := Pid}) -> + case edifa:close(Pid, edifa_opts(State)) of + {ok, State2} -> maps:remove(edifa_pid, State2); + {error, _Reason, State2} -> maps:remove(edifa_pid, State2) + end; +cleanup_image(State) -> + State. + +cleanup_temp_dir(State = #{temp_dir := TempDir, cleanup_temp_dir := true}) -> + {_, State2} = shell(State, ["rm -rf '", TempDir, "'"]), + State2#{cleanup_temp_dir => false}; +cleanup_temp_dir(State) -> + State. + + +%--- Internal ------------------------------------------------------------------ + +if_key_defined(State, Key, Result, Default) -> + case maps:find(Key, State) of + {ok, Value} when Value =/= undefined -> Result; + _ -> Default + end. + +edifa_opts(State) -> + edifa_opts(State, #{}). + +edifa_opts(State, Opts) -> + Opts#{ + log_handler => fun edifa_log_hanler/2, + log_state => State + }. + +edifa_log_hanler(Event, State) -> + event(State, [Event]). + +prepare_output_file(State, Filepath) -> + Force = maps:get(force, State, false), + case file:read_file_info(Filepath) of + {ok, #file_info{}} when Force =:= false -> + event(State, [{error, file_exists, Filepath}]); + {ok, #file_info{type = regular}} when Force =:= true-> + case file:delete(Filepath) of + ok -> State; + {error, _Reason} -> + event(State, [{error, file_access, Filepath}]) + end; + {ok, #file_info{type = regular}} -> + event(State, [{error, not_a_file, Filepath}]); + {error, enoent} -> + State + end. + +select_bootloader(State, undefined) -> + {undefined, State}; +select_bootloader(State, {directory, RootDir}) -> + case filelib:is_dir(RootDir) of + false -> + event(State, [{error, toolchain_not_found, {directory, RootDir}}]); + true -> + BaseDir = filename:join(RootDir, "barebox"), + Filenames = ?GRISP2_BOOTLOADER_FILENAMES, + select_bootloader(State, BaseDir, Filenames, fun(P, S) -> + case filelib:is_file(P) of + true -> {ok, P, S}; + false -> {error, S} + end + end) + end; +select_bootloader(State = #{temp_dir := TempDir}, {docker, ImageName}) -> + case docker_check_image(State, ImageName) of + {error, State2} -> + event(State2, [{error, toolchain_not_found, {docker, ImageName}}]); + {ok, State2} -> + BaseDir = filename:join(?DOCKER_TOOLCHAIN_ROOT, "barebox"), + Filenames = ?GRISP2_BOOTLOADER_FILENAMES, + select_bootloader(State2, BaseDir, Filenames, fun(P, S) -> + case docker_export(S, ImageName, P, TempDir) of + {error, _S2} = Error -> Error; + {ok, S2} -> + {ok, filename:join(TempDir, filename:basename(P)), S2} + end + end) + end. + +select_bootloader(State, _BaseDir, [], _CheckFun) -> + {undefined, State}; +select_bootloader(State, BaseDir, [Filename | Rest], CheckFun) -> + Path = filename:join(BaseDir, Filename), + case CheckFun(Path, State) of + {ok, Result, State2} -> {Result, State2}; + {error, State2} -> + select_bootloader(State2, BaseDir, Rest, CheckFun) + end. + +docker_check_image(State, ImageName) -> + Command = ["docker manifest inspect '", ImageName, "'"], + case shell(State, Command, [return_on_error]) of + {{ok, _}, State2} -> {ok, State2}; + {{error, _}, State2} -> {error, State2} + end. + +docker_export(State, ImageName, InPath, OutDir) -> + ok = grisp_tools_util:ensure_dir(OutDir), + Command = ["docker run --volume ", OutDir, ":", OutDir, + " ", ImageName, " sh -c \"cd ", OutDir, + " && cp -rf '", InPath, "' .\""], + case shell(State, Command, [return_on_error]) of + {{ok, _}, State2} -> {ok, State2}; + {{error, _}, State2} -> {error, State2} + end. + +format_system(State = #{edifa_pid := Pid}, PartId, Label) -> + Opts = edifa_opts(State, #{ + label => Label, + type => ?GRISP2_FAT_TYPE, + cluster_size => ?GRISP2_FAT_CLUSTER_SIZE + }), + case edifa:format(Pid, PartId, fat, Opts) of + {ok, State2} -> State2; + {error, Reason, State2} -> + event(State2, [{error, Reason}]) + end. + +deploy_bundle(State = #{edifa_pid := Pid, bundle := BundleFile}, PartId) -> + Opts = edifa_opts(State), + case edifa:mount(Pid, PartId, Opts) of + {error, Reason, State2} -> + event(State2, [{error, Reason}]); + {ok, MountPoint, State2} -> + ExpandCmd = ["tar -C ", MountPoint, " -xzf ", BundleFile], + {{ok, _}, State3} = shell(State2, ExpandCmd), + Opts2 = edifa_opts(State2), + case edifa:unmount(Pid, PartId, Opts2) of + {error, Reason, State2} -> + event(State2, [{error, Reason}]); + {ok, State3} -> + State3 + end + end.