From e20d26458208e4a11f8d3709812ef9164a32d2a9 Mon Sep 17 00:00:00 2001 From: Sebastien Merle Date: Fri, 6 Sep 2024 18:34:27 +0200 Subject: [PATCH 1/2] Minor test formating fixes --- src/rebar3_grisp_firmware.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rebar3_grisp_firmware.erl b/src/rebar3_grisp_firmware.erl index 525fb69..6e3180c 100644 --- a/src/rebar3_grisp_firmware.erl +++ b/src/rebar3_grisp_firmware.erl @@ -52,7 +52,7 @@ init(State) -> "If no bundle file is specified, it will be generated by " "calling 'rebar3 grisp deploy' with the optional release name " "and version. As for the deploy command, options passed after " - "'--' are sent to the Rebar 3 release task.\n" + "'--' are sent to the rebar 3 release task.\n" } ]), {ok, rebar_state:add_provider(State, Provider)}. @@ -283,7 +283,7 @@ event([firmware, prepare, _, {error, not_a_file, Path}]) -> event([firmware, build_firmware, create_image]) -> console("* Creating disk image..."); event([firmware, build_firmware, create_image, {error, Reason}]) -> - abort_message("Failed to create firmware image file; ~s", Reason); + abort_message("Failed to create firmware image file: ~s", Reason); event([firmware, build_firmware, copy_bootloader]) -> console("* Writing bootloader..."); event([firmware, build_firmware, copy_bootloader, {error, Reason}]) -> @@ -346,9 +346,9 @@ event(Event) -> debug("[rebar3_grisp] ~p", [Event]), case lists:last(Event) of {error, Reason, Info} when is_binary(Info) -> - abort("Unexpected 1 ~p error: ~s", [Reason, Info]); + abort("Unexpected ~p error: ~s", [Reason, Info]); {error, Reason, Info} -> - abort("Unexpected 2 ~p error: ~p", [Reason, Info]); + abort("Unexpected ~p error: ~p", [Reason, Info]); {error, Reason} -> abort("Unexpected ~p error", [Reason]); _ -> ok From 52688f008b6470e8c37eb0a4a10ebd114e805595 Mon Sep 17 00:00:00 2001 From: Sebastien Merle Date: Fri, 6 Sep 2024 18:38:24 +0200 Subject: [PATCH 2/2] Add grisp pack command --- .gitignore | 1 + CHANGELOG.md | 1 + README.md | 123 +++++++++++--- rebar.lock | 45 +++-- src/rebar3_grisp.erl | 3 +- src/rebar3_grisp_pack.erl | 341 ++++++++++++++++++++++++++++++++++++++ src/rebar3_grisp_util.erl | 47 +++++- 7 files changed, 520 insertions(+), 41 deletions(-) create mode 100644 src/rebar3_grisp_pack.erl diff --git a/.gitignore b/.gitignore index dbb38f7..24f566d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ logs _build .idea rebar3.crashdump +.tool-versions diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ca4fcd..90d8b5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New `-t/--tar` option to the deploy command to save a grisp release tarball in the `_grisp/deploy` directory. - New firmware command to generate GRiSP 2 binary firmwares: [#83](https://github.com/grisp/rebar3_grisp/pull/83) +- New pack command to generate GRiSP 2 software update package: [#85](https://github.com/grisp/rebar3_grisp/pull/85) ### Changed diff --git a/README.md b/README.md index 0fb8e8e..1e04be2 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,28 @@ rebar3 grisp deploy --tar ``` -### Generate GRiSP 2 Firmwares +### Configuration + +`rebar.config`: + +```erlang +{grisp, [ + {otp, [{version, "22.0"}]}, + {deploy, [ + % Path to put deployed release in + {destination, "/path/to/destination"}, + + % Shell script to run before deploying begins + {pre_script, "rm -rf /path/to/destination/*"}, + + % Shell script to run after deploying has finished + {post_script, "umount /path/to/destination"} + ]} +]}. +``` + + +## Generate GRiSP 2 Firmwares The `firmware` command generates binary files that can be written on GRiSP 2 eMMC. There is three types of firmware that can be generated: @@ -210,7 +231,7 @@ generation of the software bundle even if one already exists in `_grisp/deploy`: rebar3 grisp firmware --bootloader --image --force --force-bundle -#### Firmware Update +### Firmware Update Description of the variables in the commands that will follow: - **`${RELNAME}`**: The relx release names used when generating the firmware. @@ -320,9 +341,9 @@ To reset only the bootloader of the board: - Reset the GRiSP board again. -#### Cautions +### Cautions -##### With truncated image firmwares +#### With truncated image firmwares When writing a truncated eMMC image firmware, only the first system partition is written. If the active system is the second one, the board will continue to boot @@ -339,7 +360,7 @@ To change the current active system partition to the first one: $ state -s -##### With writing system firmware on inactive system partition +#### With writing system firmware on inactive system partition When writing a system firmware, be sure to do it on the active system partition or the board will continue to boot the old software. @@ -348,25 +369,89 @@ system is `/dev/mmc1.1`. See [the caution about truncated images firmware](#with for details on how to consult and change the current active system partition. -### Configuration +## Build Software Update Package + +To create a GRiSP software update package, use the 'pack' command: + + $ rebar3 grisp pack + +It creates a software update package under `_grisp/update`. + +To include the bootloader in the generate update package, add the option +`-b/--with-bootloader`: + + $ rebar3 grisp pack -b + +Note that a toolchain is required for building the bootloader firmware, see the +`deploy` command for more information on how to configure the toolchain. + +To force the recreation of the bundle and firmware(s) use the option +`-F/--force-firmware`: + + $ rebar3 grisp pack -F + +To generate a signed package, use the `-k/--key` option: + + $ rebar3 grisp pack --key private_key.pem + + +### Updating a GRiSP Board +To be able to update a GRiSP board using a software update package, the software +running on the board must have the `grisp_updater_grisp2` dependency in `rebar.config`: -```erlang -{grisp, [ - {otp, [{version, "22.0"}]}, - {deploy, [ - % Path to put deployed release in - {destination, "/path/to/destination"}, + {deps, [grisp_updater_grisp2]}. - % Shell script to run before deploying begins - {pre_script, "rm -rf /path/to/destination/*"}, +`grisp_updater` needs to be configured in `sys.config` (or equivalent): + + {grisp_updater, [ + {signature_check, true}, + {signature_certificates, {priv, my_app, "certificates/updates"}}, + {system, {grisp_updater_grisp2, #{}}}, + {sources, [ + {grisp_updater_tarball, #{}}, + {grisp_updater_http, #{ + backend => {grisp_updater_grisp2, #{}} + }} + ]} + ]}, + +If `signature_check` is set to `true` the software package must be signed using +the `-k/--key` option, and the public key must be available in the directory +configured by `signature_certificates`. + +When these conditions are met, you can follow these step to perform a A/B +software update of a GRiSP board: + + - Unpack the software update package in some local directory: + + **`any`** `$ mkdir -p releases/${RELNAME}/${RELVSN}` + **`any`** `$ tar -C releases/${RELNAME}/${RELVSN} -xvf _grisp/update/grisp2.${REL_NAME}".${RELVSN}.${PROFILE}.tar -xvf` + + - Start a local HTTP server to serve the package: + + **`any`** `$ http-server ./releases -p 8000` + + - Open a serial console to the GRiSP board: + + **`macOS`** `$ screen /dev/tty.usbserial-0${GRISP_BOARD_SERIAL}1 115200` + + **`Linux`** `$ screen /dev/ttyUSB1 115200` + +- On the GRiSP2 console, start the update process: + + **`GRiSP`** `$ grisp_updater:update(<<"http://${HOST_IP}:8000/${RELNAME}/${RELVSN}">>).` + +- Reset the GRiSP2 board using the onboard reset button. + +- Validate the new software version on the GRiSP2 console: + + **`GRiSP`** `$ grisp_updater:validate().` + +Note that the update process will only show progress information if the log +level is at least `INFO`. - % Shell script to run after deploying has finished - {post_script, "umount /path/to/destination"} - ]} -]}. -``` ## Listing Packages diff --git a/rebar.lock b/rebar.lock index c11800f..3072cd5 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,41 +1,62 @@ {"1.2.0", [{<<"bbmustache">>,{pkg,<<"bbmustache">>,<<"1.12.2">>},1}, {<<"certifi">>,{pkg,<<"certifi">>,<<"2.12.0">>},2}, - {<<"grid">>,{pkg,<<"grid">>,<<"0.1.0">>},0}, - {<<"grisp_tools">>,{pkg,<<"grisp_tools">>,<<"2.6.1">>},0}, - {<<"hackney">>,{pkg,<<"hackney">>,<<"1.18.2">>},1}, + {<<"edifa">>,{pkg,<<"edifa">>,<<"1.0.0">>},1}, + {<<"erlexec">>,{pkg,<<"erlexec">>,<<"2.0.7">>},2}, + {<<"grid">>,{pkg,<<"grid">>,<<"0.2.1">>},0}, + {<<"grisp_tools">>,{pkg,<<"grisp_tools">>,<<"2.7.0">>},0}, + {<<"grisp_update_packager">>,{pkg,<<"grisp_update_packager">>,<<"1.0.0">>},1}, + {<<"hackney">>,{pkg,<<"hackney">>,<<"1.20.1">>},1}, {<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},2}, {<<"mapz">>,{pkg,<<"mapz">>,<<"2.4.0">>},1}, {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2}, {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.3.0">>},2}, {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.4.1">>},2}, + {<<"quickrand">>,{pkg,<<"quickrand">>,<<"2.0.7">>},3}, {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.7">>},2}, - {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},2}]}. + {<<"termseal">>,{pkg,<<"termseal">>,<<"0.1.1">>},2}, + {<<"textual">>,{pkg,<<"textual">>,<<"0.1.1">>},1}, + {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},2}, + {<<"uuid">>,{pkg,<<"uuid_erl">>,<<"2.0.7">>},2}]}. [ {pkg_hash,[ {<<"bbmustache">>, <<"0CABDCE0DB9FE6D3318131174B9F2B351328A4C0AFBEB3E6E99BB0E02E9B621D">>}, {<<"certifi">>, <<"2D1CCA2EC95F59643862AF91F001478C9863C2AC9CB6E2F89780BFD8DE987329">>}, - {<<"grid">>, <<"880F3D2E5B4E9B3B93FDE2BC93F11F6CD6DE82BE886D3AF026C2D6A17C762907">>}, - {<<"grisp_tools">>, <<"492F579EE450C7618EDB118A5BBC3F8FE00EC0073305246B80C444C1B176EE8C">>}, - {<<"hackney">>, <<"D7FF544DDAE5E1CB49E9CF7FA4E356D7F41B283989A1C304BFC47A8CC1CF966F">>}, + {<<"edifa">>, <<"0F1A01A0C79B7135F334B3FCEEB624F0574C5ED3E4554B06C8664AADA6A339C8">>}, + {<<"erlexec">>, <<"76D0BC7487929741B5BB9F74DA2AF5DAF1492134733CF9A05C7AAA278B6934C5">>}, + {<<"grid">>, <<"4DCBF6155AB24131CB493D417F28093F019197ED7401F28BF82FF60E2C9B0D2C">>}, + {<<"grisp_tools">>, <<"2FE6F2CDDB18D7D207CAAAF2ADEDBE0D2533163469D27DDFC5B9AF05F5E41167">>}, + {<<"grisp_update_packager">>, <<"0532CCD0955398FAC4E1DE90FE85DB941CA609A2F4E066CFFE01ECE41DCCE119">>}, + {<<"hackney">>, <<"8D97AEC62DDDDD757D128BFD1DF6C5861093419F8F7A4223823537BAD5D064E2">>}, {<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>}, {<<"mapz">>, <<"77A8E38B69BAB16C5D3EBD44E6C619F8AF1F1598B0CAAE301D266605A0865756">>}, {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, {<<"mimerl">>, <<"D0CD9FC04B9061F82490F6581E0128379830E78535E017F7780F37FEA7545726">>}, {<<"parse_trans">>, <<"6E6AA8167CB44CC8F39441D05193BE6E6F4E7C2946CB2759F015F8C56B76E5FF">>}, + {<<"quickrand">>, <<"D2BD76676A446E6A058D678444B7FDA1387B813710D1AF6D6E29BB92186C8820">>}, {<<"ssl_verify_fun">>, <<"354C321CF377240C7B8716899E182CE4890C5938111A1296ADD3EC74CF1715DF">>}, - {<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>}]}, + {<<"termseal">>, <<"C9D93D4FF638EE99F9377D3438FC7AD132D2901EBBAF10C54F8DEA1D7E24D61C">>}, + {<<"textual">>, <<"42D6AFE1E58F128E607C237EC213CD3DD69B780A3527039B2A90CA6600456B3C">>}, + {<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>}, + {<<"uuid">>, <<"B2078D2CC814F53AFA52D36C91E08962C7E7373585C623F4C0EA6DFB04B2AF94">>}]}, {pkg_hash_ext,[ {<<"bbmustache">>, <<"688B33A4D5CC2D51F575ADF0B3683FC40A38314A2F150906EDCFC77F5B577B3B">>}, {<<"certifi">>, <<"EE68D85DF22E554040CDB4BE100F33873AC6051387BAF6A8F6CE82272340FF1C">>}, - {<<"grid">>, <<"E71751225A9DFF8C7C96551CC181C0FDD8D8C666D3C2FE44A832D6F8B7BE0013">>}, - {<<"grisp_tools">>, <<"7642DE1A466B92AE70C0924605211AD126F65B0F2BFC56069FA996C5CCC78104">>}, - {<<"hackney">>, <<"AF94D5C9F97857DB257090A4A10E5426ECB6F4918AA5CC666798566AE14B65FD">>}, + {<<"edifa">>, <<"A1E010561E7D236A24C668D95626BE2BFE082ED0331CE1E6798BE0CD43F59A7B">>}, + {<<"erlexec">>, <<"AF2DD940BB8E32F5AA40A65CB455DCAA18F5334FD3507E9BFD14A021E9630897">>}, + {<<"grid">>, <<"C8EA819A0E40631BECE3149FBA7D306DF6CF8BC35358089878F05B20E6D87D4C">>}, + {<<"grisp_tools">>, <<"7B40D67BF53B950C323E92DC8860252052D776A527FC2F4CC9DBEDDF0CFFE949">>}, + {<<"grisp_update_packager">>, <<"47BFDF6FADBED4B8342205A812198CF913E0223A98A775CAAE5D2FB5D5CF751C">>}, + {<<"hackney">>, <<"FE9094E5F1A2A2C0A7D10918FEE36BFEC0EC2A979994CFF8CFE8058CD9AF38E3">>}, {<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>}, {<<"mapz">>, <<"4B68DF5CF0522E0D6545DF7B681BC052865CDB78405AD4CC9C55FE45EE7B25BE">>}, {<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>}, {<<"mimerl">>, <<"A1E15A50D1887217DE95F0B9B0793E32853F7C258A5CD227650889B38839FE9D">>}, {<<"parse_trans">>, <<"620A406CE75DADA827B82E453C19CF06776BE266F5A67CFF34E1EF2CBB60E49A">>}, + {<<"quickrand">>, <<"B8ACBF89A224BC217C3070CA8BEBC6EB236DBE7F9767993B274084EA044D35F0">>}, {<<"ssl_verify_fun">>, <<"FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8">>}, - {<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}]} + {<<"termseal">>, <<"466280936214AF1894FC431642E83341B7D13580A3F3485820A2D300C5CAEB49">>}, + {<<"textual">>, <<"28C1AE5DE77D3A13C9101DD64204C87069232149C2B924762F43D75730516CE9">>}, + {<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}, + {<<"uuid">>, <<"4E4C5CA3461DC47C5E157ED42AA3981A053B7A186792AF972A27B14A9489324E">>}]} ]. diff --git a/src/rebar3_grisp.erl b/src/rebar3_grisp.erl index 9d5716d..5dcf6f6 100644 --- a/src/rebar3_grisp.erl +++ b/src/rebar3_grisp.erl @@ -19,5 +19,6 @@ init(State) -> rebar3_grisp_package, rebar3_grisp_version, rebar3_grisp_report, - rebar3_grisp_firmware + rebar3_grisp_firmware, + rebar3_grisp_pack ]). diff --git a/src/rebar3_grisp_pack.erl b/src/rebar3_grisp_pack.erl new file mode 100644 index 0000000..5d6c5b0 --- /dev/null +++ b/src/rebar3_grisp_pack.erl @@ -0,0 +1,341 @@ +-module(rebar3_grisp_pack). + +% Callbacks +-export([init/1]). +-export([do/1]). +-export([format_error/1]). + +-import(rebar3_grisp_util, [ + debug/2, + info/1, + info/2, + console/1, + console/2, + warn/1, + warn/2, + abort/1, + abort/2 +]). + +-define(MAX_DDOT, 2). + +%--- Callbacks ----------------------------------------------------------------- + +-spec init(rebar_state:t()) -> {ok, rebar_state:t()}. +init(State) -> + Provider = providers:create([ + {namespace, grisp}, + {name, pack}, + {module, ?MODULE}, + {bare, true}, + {deps, [{default, install_deps}, {default, compile}]}, + {example, "rebar3 grisp pack"}, + {opts, [ + {relname, $n, "relname", string, "Specify the name for the release that will be deployed"}, + {relvsn, $v, "relvsn", string, "Specify the version of the release"}, + {system, undefined, "system", string, "An explicit system firmware to use in the update package"}, + {bootloader, undefined, "bootloader", string, "An explicit bootloader firmware to use in the update package"}, + {block_size, undefined, "block-size", integer, "The size of the blocks in bytes, before compression"}, + {key, $k, "key", string, "Private key PEM file to use to sign the update package"}, + {with_bootloader, $b, "with-bootloader", {boolean, false}, "Include a bootloader firmware in the software update package"}, + {force_firmware, $F, "force-firmware", {boolean, false}, "Force firmware building even if it already exists"}, + {force, $f, "force", {boolean, false}, "Replace existing files"}, + {quiet, $q, "quiet", {boolean, false}, "Do not show the instructions on how to update a GRiSP2 board"} + ]}, + {profiles, [grisp]}, + {short_desc, "Generate a GRiSP software update package"}, + {desc, + "Generate a tarball that can be used with grisp software update.\n" + "\n" + "If no firmware file is specified, it will be generated by " + "calling 'rebar3 grisp firmware' with the optional release name " + "and version. As for the firmware command, options passed after " + "'--' are sent to the rebar 3 release task.\n" + } + ]), + {ok, rebar_state:add_provider(State, Provider)}. + +-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. +do(RState) -> + try + {Args, ExtraArgs} = rebar_state:command_parsed_args(RState), + RelNameArg = proplists:get_value(relname, Args, undefined), + RelVsnArg = proplists:get_value(relvsn, Args, undefined), + {RelName, RelVsn} + = rebar3_grisp_util:select_release(RState, RelNameArg, RelVsnArg), + case {proplists:get_value(firmware, Args, undefined), + proplists:get_value(bootloader, Args, undefined), + proplists:get_value(with_bootloader, Args)} of + {undefined, undefined, true} -> + Force = proplists:get_value(force_firmware, Args, false), + case get_firmwares(RState, Force, RelName, RelVsn, ExtraArgs) of + {error, _Reason} = Error -> Error; + {ok, SysFile, BootFile, RState2} -> + grisp_tools_pack(RState2, RelName, RelVsn, + [{system, SysFile}, + {bootloader, BootFile} | Args]) + end; + {undefined, undefined, false} -> + Force = proplists:get_value(force_firmware, Args, false), + case get_firmware(RState, Force, RelName, RelVsn, ExtraArgs) of + {error, _Reason} = Error -> Error; + {ok, SysFile, RState2} -> + grisp_tools_pack(RState2, RelName, RelVsn, + [{system, SysFile} | Args]) + end; + {SysFile, undefined, false} + when SysFile =/= undefined -> + case filelib:is_file(SysFile) of + false -> + abort("System firmware file not found: ~s", [SysFile]); + true -> + SysRelPath = grisp_tools_util:maybe_relative(SysFile, ?MAX_DDOT), + console("* Using provided system firmware: ~s", [SysRelPath]), + grisp_tools_pack(RState, RelName, RelVsn, Args) + end; + {SysFile, BootFile, _} + when SysFile =/= undefined, BootFile =/= undefined -> + case {filelib:is_file(SysFile), + filelib:is_file(BootFile)} of + {false, _} -> + abort("System firmware file not found: ~s", [SysFile]); + {_, false} -> + abort("Bootloader firmware file not found: ~s", [BootFile]); + {true, true} -> + SysRelPath = grisp_tools_util:maybe_relative(SysFile, ?MAX_DDOT), + BootRelPath = grisp_tools_util:maybe_relative(BootFile, ?MAX_DDOT), + console("* Using provided system firmware: ~s", [SysRelPath]), + console("* Using provided bootloader firmware: ~s", [BootRelPath]), + grisp_tools_pack(RState, RelName, RelVsn, Args) + end; + _ -> + abort("When including the bootloader firmware and an explicit system firmware, both must be explicit; " + "the options --system and --bootloader must be specified together.", []) + end + catch + error:{release_not_selected, [{Name, [Version|_]}|_]} -> + abort( + "Multiple releases defined!~n" + "You must specify a name and optionally a version. Examples:~n" + "~n" + " rebar3 grisp pack --relname ~p~n" + " rebar3 grisp pack --relname ~p --relvsn ~s~n", + [Name, Name, Version] + ); + error:no_release_configured -> + App = rebar_app_info:name(hd(rebar_state:project_apps(RState))), + abort( + "No release configured" + "~n" + "You must specify at least one release in 'rebar.config' to be " + "able to create a software update package.~nExample:~n" + "~n" + " {relx,~n" + " {~s, \"0.1.0\", [~s]}~n" + " }.~n", + [App, App] + ); + error:{unknown_release_name, {Name, _Version}, Names} -> + abort( + "Unknown release '~p'~n" + "~n" + "Must be one of:" ++ [["~n ", atom_to_list(N)] || N <- Names], + [Name] + ); + error:{unknown_release_version, {Name, Version}, Versions} -> + abort( + "Release '~p' has no version ~s~n" + "~n" + "Must be one of:" ++ [["~n ", V] || V <- Versions], + [Name, Version] + ); + error:{create_dir_failed, Dir, {error, Reason}} -> + abort( + "Error creating directory ~s: ~s", + [Dir, file:format_error(Reason)] + ) + end. + +-spec format_error(any()) -> iolist(). +format_error(Reason) -> + io_lib:format("~p", [Reason]). + + +%--- Internal ------------------------------------------------------------------ + +get_firmwares(RState, Force, RelName, RelVsn, ExtraRelArgs) -> + SysFile + = rebar3_grisp_util:firmware_file_path(RState, system, RelName, RelVsn), + BootFile + = rebar3_grisp_util:firmware_file_path(RState, boot, RelName, RelVsn), + case {filelib:is_file(SysFile), filelib:is_file(BootFile)} of + {true, true} when Force =:= false -> + SysRelPath = grisp_tools_util:maybe_relative(SysFile, ?MAX_DDOT), + BootRelPath = grisp_tools_util:maybe_relative(BootFile, ?MAX_DDOT), + console("* Using existing system firmware: ~s", [SysRelPath]), + console("* Using existing bootloader firmware: ~s", [BootRelPath]), + {ok, SysFile, BootFile, RState}; + _ -> + console("* Building firmwares...", []), + case build_firmwares(RState, true, Force, RelName, + RelVsn, ExtraRelArgs) of + {ok, RState2} -> {ok, SysFile, BootFile, RState2}; + {error, _Reason} = Error -> Error + end + end. + +get_firmware(RState, Force, RelName, RelVsn, ExtraRelArgs) -> + SysFile + = rebar3_grisp_util:firmware_file_path(RState, system, RelName, RelVsn), + case filelib:is_file(SysFile) of + true when Force =:= false -> + SysRelPath = grisp_tools_util:maybe_relative(SysFile, ?MAX_DDOT), + console("* Using existing system firmware: ~s", [SysRelPath]), + {ok, SysFile, RState}; + _ -> + console("* Building system firmware...", []), + case build_firmwares(RState, false, Force, RelName, + RelVsn, ExtraRelArgs) of + {ok, RState2} -> {ok, SysFile, RState2}; + {error, _Reason} = Error -> Error + end + end. + +build_firmwares(RState, WithBoot, Force, RelName, RelVsn, ExtraRelArgs) -> + Args = [ + "--relname", atom_to_list(RelName), + "--relvsn", RelVsn, + "--force", + "--quiet" + ] ++ case WithBoot of + true -> ["--bootloader"]; + false -> [] + end ++ case Force =:= true of + true -> ["--force-bundle"]; + false -> [] + end ++ case ExtraRelArgs of + [_|_] -> ["--" | ExtraRelArgs]; + _ -> [] + end, + rebar3_grisp_util:rebar_command(RState, grisp, firmware, Args). + +grisp_tools_pack(RState, RelName, RelVsn, Args) -> + SysFile = proplists:get_value(system, Args), + BootFile = proplists:get_value(bootloader, Args), + Force = proplists:get_value(force, Args, false), + KeyFile = proplists:get_value(key, Args), + BlockSize = proplists:get_value(block_size, Args), + PackageFile = rebar3_grisp_util:update_file_path(RState, RelName, RelVsn), + + PackSpec = #{ + name => atom_to_binary(RelName), + version => RelVsn, + block_size => BlockSize, + key_file => KeyFile, + system => SysFile, + bootloader => BootFile, + package => PackageFile, + force => Force, + handlers => grisp_tools:handlers_init(#{ + event => {fun event_handler/2, RState}, + shell => {fun rebar3_grisp_handler:shell/3, #{}} + }) + }, + State = grisp_tools:pack(PackSpec), + #{event := RState2} = grisp_tools:handlers_finalize(State), + info("Package created"), + case proplists:get_value(quiet, Args) of + false -> info("~s", [update_usage(RState2, RelName, RelVsn)]); + true -> ok + end, + {ok, RState2}. + +event_handler(Event, RState) -> + event(Event), + {ok, RState}. + +event([pack, prepare]) -> + console("* Preparing and validating..."); +event([pack, prepare, _, {error, firmware_not_found, Path}]) -> + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), + abort("System firmware file not found: ~s", [RelPath]); +event([pack, prepare, _, {error, bootloader_not_found, Path}]) -> + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), + abort("Bootloader firmware file not found: ~s", [RelPath]); +event([pack, prepare, _, {error, file_exists, Path}]) -> + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), + abort("File already exists (use --force to overwrite): ~s", [RelPath]); +event([pack, prepare, _, {error, file_access, Path}]) -> + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), + abort("File not accessible: ~s", [RelPath]); +event([pack, prepare, _, {error, not_a_file, Path}]) -> + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), + abort("Not a regular file: ~s", [RelPath]); +event([pack, package, _, {expanding, Filename}]) -> + console("* Expanding compressed file ~s", [Filename]); +event([pack, package, build_package]) -> + console("* Creating software update package..."); +event([pack, package, _, {done, Path}]) -> + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), + console(" Software update package generated: ~s", [RelPath]); +event([pack, package, _, {error, Reason}]) -> + abort_message("Failed to create package: ~s", Reason); +event(Event) -> + debug("[rebar3_grisp] ~p", [Event]), + case lists:last(Event) of + {error, Reason, Info} when is_binary(Info) -> + abort("Unexpected ~p error: ~s", [Reason, Info]); + {error, Reason, Info} -> + abort("Unexpected ~p error: ~p", [Reason, Info]); + {error, Reason} -> + abort("Unexpected ~p error", [Reason]); + _ -> ok + end. + +abort_message(Prefix, Msg) when is_binary(Msg) -> + abort("~s; ~s", [Prefix, Msg]); +abort_message(Prefix, Reason) -> + abort("~s: ~p", [Prefix, Reason]). + +local_address() -> + case inet:getifaddrs() of + {ok, Addrs} -> + case [A || {_, P} <- Addrs, + A <- [proplists:get_value(addr, P)], + {_, _, _, _} <- [A], + A =/= {127, 0, 0, 1}] of + [] -> <<"HOSTNAME">>; + [Addr | _] -> inet_parse:ntoa(Addr) + end; + _ -> + <<"HOSTNAME">> + end. + +update_usage(RState, RelName0, RelVsn) -> + RelName = atom_to_binary(RelName0), + PackageFile0 = rebar3_grisp_util:update_file_path(RState, RelName, RelVsn), + PackageFile = grisp_tools_util:maybe_relative(PackageFile0, ?MAX_DDOT), + iolist_to_binary([ + "Instructions to update GRiSP2 software on the board with grisp_updater:\n", + " NOTE: the software running on the board MUST run grisp_updater by adding the grisp_updater_grisp2 dependency.\n", + " - Unpack the software update package in some local directory:\n", + " $ mkdir -p releases/", RelName, "/", RelVsn, "\n", + " $ tar -C releases/", RelName, "/", RelVsn, " -xvf ", PackageFile, "\n", + " - Start a local HTTP server to serve the package:\n", + " $ http-server ./releases -p 8000\n", + case os:type() of + {unix, darwin} -> + [" - Open a serial console to the GRiSP board:\n", + " $ screen /dev/tty.usbserial-0${GRISP_BOARD_SERIAL}1 115200\n"]; + {unix, linux} -> + [" - Open a serial console to the GRiSP board:\n", + " $ screen /dev/ttyUSB1 115200\n"]; + _ -> + [" - Open a serial console to the GRiSP board.\n"] + end, + " - On the GRiSP2 console, start the update process:\n", + " $ grisp_updater:update(<<\"http://", local_address(), ":8000/", RelName, "/", RelVsn, "\">>).\n", + " - Reset the GRiSP2 board using the onboard reset button.\n", + " - Validate the new software version on the GRiSP2 console:\n", + " $ grisp_updater:validate().\n" + ]). diff --git a/src/rebar3_grisp_util.erl b/src/rebar3_grisp_util.erl index 3c2dcd3..47e4d3e 100644 --- a/src/rebar3_grisp_util.erl +++ b/src/rebar3_grisp_util.erl @@ -39,6 +39,9 @@ -export([firmware_dir/1]). -export([firmware_file_name/4]). -export([firmware_file_path/4]). +-export([update_dir/1]). +-export([update_file_name/3]). +-export([update_file_path/3]). -export([toolchain_root/1]). -import(rebar3_grisp_tools, [event/2]). @@ -195,8 +198,9 @@ select_release(RebarState, RelName0, RelVsn) bundle_file_name(RebarState, RelName, RelVsn) -> Config = config(RebarState), Board = platform(Config), - iolist_to_binary(io_lib:format("~s.~s.~s.tar.gz", - [Board, RelName, RelVsn])). + Profile = profile_postfix(RebarState), + iolist_to_binary(io_lib:format("~s.~s.~s~s.tar.gz", + [Board, RelName, RelVsn, Profile])). bundle_file_path(RebarState, RelName, RelVsn) -> BundleDir = rebar3_grisp_util:deploy_dir(RebarState), @@ -204,29 +208,47 @@ bundle_file_path(RebarState, RelName, RelVsn) -> filename:join(BundleDir, BundleName). firmware_dir(RebarState) -> - filename:join([root(RebarState), "firmware"]). + filename:join([root(RebarState), "firmware"]). firmware_file_name(RebarState, image, RelName, RelVsn) -> Config = config(RebarState), Board = platform(Config), - iolist_to_binary(io_lib:format("~s.~s.~s.emmc.gz", - [Board, RelName, RelVsn])); + Profile = profile_postfix(RebarState), + iolist_to_binary(io_lib:format("~s.~s.~s~s.emmc.gz", + [Board, RelName, RelVsn, Profile])); firmware_file_name(RebarState, system, RelName, RelVsn) -> Config = config(RebarState), Board = platform(Config), - iolist_to_binary(io_lib:format("~s.~s.~s.sys.gz", - [Board, RelName, RelVsn])); + Profile = profile_postfix(RebarState), + iolist_to_binary(io_lib:format("~s.~s.~s~s.sys.gz", + [Board, RelName, RelVsn, Profile])); firmware_file_name(RebarState, boot, RelName, RelVsn) -> Config = config(RebarState), Board = platform(Config), - iolist_to_binary(io_lib:format("~s.~s.~s.boot.gz", - [Board, RelName, RelVsn])). + Profile = profile_postfix(RebarState), + iolist_to_binary(io_lib:format("~s.~s.~s~s.boot.gz", + [Board, RelName, RelVsn, Profile])). firmware_file_path(RebarState, Type, RelName, RelVsn) -> BundleDir = rebar3_grisp_util:firmware_dir(RebarState), BundleName = firmware_file_name(RebarState, Type, RelName, RelVsn), filename:join(BundleDir, BundleName). +update_dir(RebarState) -> + filename:join([root(RebarState), "update"]). + +update_file_name(RebarState, RelName, RelVsn) -> + Config = config(RebarState), + Board = platform(Config), + Profile = profile_postfix(RebarState), + iolist_to_binary(io_lib:format("~s.~s.~s~s.tar", + [Board, RelName, RelVsn, Profile])). + +update_file_path(RebarState, RelName, RelVsn) -> + UpdateDir = rebar3_grisp_util:update_dir(RebarState), + UpdateName = update_file_name(RebarState, RelName, RelVsn), + filename:join(UpdateDir, UpdateName). + rebar_command(RebarState, Namespace, Command, Args) -> % Backup current command state OriginalNamespace = rebar_state:namespace(RebarState), @@ -307,3 +329,10 @@ index_releases(Releases) -> ) end, Versions)} end, Index). + +profile_postfix(RebarState) -> + AllProfiles = rebar_state:current_profiles(RebarState), + case [atom_to_binary(P) || P <- AllProfiles, P =/= default, P =/= grisp] of + [] -> <<"">>; + Profiles -> iolist_to_binary([".", lists:join($+, Profiles)]) + end.