diff --git a/CHANGELOG.md b/CHANGELOG.md index e4132ca..0ca4fcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,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) ### Changed diff --git a/README.md b/README.md index c1950ff..52793b1 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,148 @@ all the files will be bundled in a a tarball under `_grisp/deploy`: rebar3 grisp deploy --tar ``` + +### 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: + + - **System Firmware**: + The system firmware is the content of a system partition on the eMMC. + When using A/B software update, the system firmware can be written either + on the first or the second system partition. By default, the command will + generate a system firmware into `_grisp/firmware` but it can be disabled with + the option `-b false` or `--system false`. + - **eMMC Image Firmware**: + The eMMC image firmware is a full image containing the bootloader, the + partition table and the system partitions. It is meant to be written on the + GRiSP 2 board to reset it completely with the new software. If the image is + truncated (it is by default), the image only contains the first system + partition. It means that when writing the firmware to the eMMC, the second + system partition will be untouched. To generate an eMMC image firmware into + `_grisp/firmware`, add the option `-i` or `--image`, to disable truncating + so the image contains both system partitions, uses the option `-t false` or + `--truncate false`. + - **Bootloader Firmware**: + The bootloader firmware contains only the bootloader and the partition table. + To generate it under `_grisp/firmware`, add the option `-b` or `--bootloader`. + +e.g. + +Generate a system firmware for the default release: + + rebar3 grisp firmware + +Generate a system firmware for a specific release: + + rebar3 grisp firmware --relname myapp --relvsn 1.2.3 + +Generate all firmwares, forcing existing files to be overwritten and forcing the +generation of the software bundle even if one already exists in `_grisp/deploy`: + + rebar3 grisp firmware --bootloader --image --force --force-bundle + + +#### Firmware Update + +Description of the variables in the commands that will follow: + - **`${RELNAME}`**: The relx release names used when generating the firmware. + - **`${RELVSN}`**: The relx release version used when generating the firmware. + - **`${USER}`**: The username of the account running the command. + - **`${GRISP_BOARD_SERIAL}`**: The serial number of the GRiSP 2 board. + +To write a system firmware to a GRiSP 2 board: + + - Copy the firmware to the SD card: + **`macOS`** `$ cp _grisp/firmware/grisp2.${RELNAME}.${RELVSN}.sys.gz /Volumes/GRISP` + **`Linux`** `$ cp _grisp/firmware/grisp2.${RELNAME}.${RELVSN}.sys.gz /media/${USER}/GRISP` + - Unmount the SD card: + **`macOS`** `$ diskutil umount /Volumes/GRISP` + **`Linux`** `$ umount /media/${USER}/GRISP` + - Open a serial console to the GRiSP board: + **`macOS`** `$ screen /dev/tty.usbserial-0${GRISP_BOARD_SERIAL}1 115200` + **`Linux`** `$ screen /dev/ttyUSB1 115200` + - Insert the SD card in the GRiSP 2 board. + - Reset the board using the onboard reset button. + - Enter into barebox console mode by pressing any key before 3 seconds. + - Consult the current active system partition: + **`Barebox`** `$ echo $state.bootstate.active_system` + - Write the firmware. If the current active system is `0`, use device + `/dev/mmc1.0`, if it is `1` use device `/dev/mmc1.1`: + **`Barebox`** `$ uncompress /mnt/mmc/grisp2.${RELNAME}.${RELVSN}.sys.gz /dev/mmc1.0` + - Remove the SD card. + - Reset the GRiSP board again. + +To reset a GRiSP 2 board eMMC, either with a truncated or full image firmware: + + - Copy the firmware to the SD card: + **`macOS`** `$ cp _grisp/firmware/grisp2.${RELNAME}.${RELVSN}.emmc.gz /Volumes/GRISP` + **`Linux`** `$ cp _grisp/firmware/grisp2.${RELNAME}.${RELVSN}.emmc.gz /media/${USER}/GRISP` + - Unmount the SD card: + **`macOS`** `$ diskutil umount /Volumes/GRISP` + **`Linux`** `$ umount /media/${USER}/GRISP` + - Open a serial console to the GRiSP board: + **`macOS`** `$ screen /dev/tty.usbserial-0${GRISP_BOARD_SERIAL}1 115200` + **`Linux`** `$ screen /dev/ttyUSB1 115200` + - Insert the SD card in the GRiSP 2 board. + - Reset the board using the onboard reset button. + - Enter into barebox console mode by pressing any key before 3 seconds. + - Set the current active system partition to the first one: + **`Barebox`** `$ let state.bootstate.active_system=0` + **`Barebox`** `$ state -s` + - Write the firmware: + **`Barebox`** `$ uncompress /mnt/mmc/grisp2.${RELNAME}.${RELVSN}.emmc.gz /dev/mmc1` + - Remove the SD card. + - Reset the GRiSP board again. + +To reset only the bootloader of the board: + + - Copy the firmware to the SD card: + **`macOS`** `$ cp _grisp/firmware/grisp2.${RELNAME}.${RELVSN}.boot.gz /Volumes/GRISP` + **`Linux`** `$ cp _grisp/firmware/grisp2.${RELNAME}.${RELVSN}.boot.gz /media/${USER}/GRISP` + - Unmount the SD card: + **`macOS`** `$ diskutil umount /Volumes/GRISP` + **`Linux`** `$ umount /media/${USER}/GRISP` + - Open a serial console to the GRiSP board: + **`macOS`** `$ screen /dev/tty.usbserial-0${GRISP_BOARD_SERIAL}1 115200` + **`Linux`** `$ screen /dev/ttyUSB1 115200` + - Insert the SD card in the GRiSP 2 board. + - Reset the board using the onboard reset button. + - Enter into barebox console mode by pressing any key before 3 seconds. + - Write the firmware: + **`Barebox`** `$ uncompress /mnt/mmc/grisp2.${RELNAME}.${RELVSN}.boot.gz /dev/mmc1` + - Remove the SD card. + - Reset the GRiSP board again. + + +#### Cautions + +##### 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 +the old software. You will need to manually change the active system partition +in the bootloader console and restart the board. + +To consule the current active system partition in the bootloader console: + + $ echo $state.bootstate.active_system + +To change the current active system partition to the first one: + + $ let state.bootstate.active_system=0 + $ state -s + + +##### 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. +The device for the first system is `/dev/mmc1.0` and the one for the second +system is `/dev/mmc1.1`. See [the caution about truncated images firmware](#with-truncated-image-firmwares) +for details on how to consult and change the current active system partition. + + ### Configuration `rebar.config`: diff --git a/src/rebar3_grisp.erl b/src/rebar3_grisp.erl index 9d87016..9d5716d 100644 --- a/src/rebar3_grisp.erl +++ b/src/rebar3_grisp.erl @@ -18,5 +18,6 @@ init(State) -> rebar3_grisp_configure, rebar3_grisp_package, rebar3_grisp_version, - rebar3_grisp_report + rebar3_grisp_report, + rebar3_grisp_firmware ]). diff --git a/src/rebar3_grisp_build.erl b/src/rebar3_grisp_build.erl index dc20670..6d6c5d0 100644 --- a/src/rebar3_grisp_build.erl +++ b/src/rebar3_grisp_build.erl @@ -56,7 +56,7 @@ do(RState) -> Apps = rebar3_grisp_util:apps(RState), ProjectRoot = rebar_dir:root_dir(RState), - ToolchainRoot = toolchain_root(Config), + ToolchainRoot = toolchain_root(RState), Flags = maps:from_list([ {F, rebar3_grisp_util:get(F, Opts, D)} @@ -113,28 +113,16 @@ format_error(Reason) -> %--- Internal ------------------------------------------------------------------ -toolchain_root(Config) -> - DockerImg = rebar3_grisp_util:get([build, toolchain, docker], Config, error), - TCDir = rebar3_grisp_util:get([build, toolchain, directory], Config, error), - case os:getenv("GRISP_TOOLCHAIN", TCDir) of - error -> case DockerImg of - error -> abort_no_toolchain(); - DockerImage -> - ensure_docker(), - {docker, DockerImage} - end; - Directory -> - {directory, Directory} - end. - -ensure_docker() -> - case rebar3_grisp_util:sh("docker info",[return_on_error]) of - {error, _} -> abort("Docker is not available"); - {ok, _} -> ok +toolchain_root(RebarState) -> + case rebar3_grisp_util:toolchain_root(RebarState) of + undefined -> abort_no_toolchain(); + {docker, _DockerImage} = Result -> Result; + {directory, _Directory} = Result -> Result; + {error, docker_not_found} -> abort("Docker is not available") end. abort_no_toolchain() -> - "Please specify the full path to the toolchain directory in your rebar.conf: + abort("Please specify the full path to the toolchain directory in your rebar.conf: {grisp, [ ..., @@ -147,7 +135,7 @@ abort_no_toolchain() -> ]} ]}. -Alternatively, you can set the GRISP_TOOLCHAIN environment variable.". +Alternatively, you can set the GRISP_TOOLCHAIN environment variable."). abort_no_build() -> abort("There was no build section found in your rebar.conf"). diff --git a/src/rebar3_grisp_deploy.erl b/src/rebar3_grisp_deploy.erl index 1501cb8..0dffa83 100644 --- a/src/rebar3_grisp_deploy.erl +++ b/src/rebar3_grisp_deploy.erl @@ -17,6 +17,8 @@ abort/2 ]). +-define(MAX_DDOT, 2). + %--- Callbacks ----------------------------------------------------------------- -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. @@ -60,6 +62,8 @@ do(RState) -> {Args, _} = rebar_state:command_parsed_args(RState), Force = proplists:get_value(force, Args, false), Tar = proplists:get_value(tar, Args, false), + RelNameArg = proplists:get_value(relname, Args), + RelVsnArg = proplists:get_value(relvsn, Args), ProjectRoot = rebar_dir:root_dir(RState), Apps = rebar3_grisp_util:apps(RState), @@ -67,7 +71,8 @@ do(RState) -> CustomBuild = rebar3_grisp_util:should_build(Config), try - {RelName, RelVsn} = select_release(Args, RState), + {RelName, RelVsn} + = rebar3_grisp_util:select_release(RState, RelNameArg, RelVsnArg), DistSpec = case {Tar, CopyDest} of {false, D} when D =:= undefined; D =:= "" -> error(no_deploy_destination); @@ -125,8 +130,8 @@ do(RState) -> "Multiple releases defined!~n" "You must specify a name and optionally a version. Examples:~n" "~n" - " rebar3 grisp release --relname ~p~n" - " rebar3 grisp release --relname ~p --relvsn ~s~n", + " rebar3 grisp deploy --relname ~p~n" + " rebar3 grisp deploy --relname ~p --relvsn ~s~n", [Name, Name, Version] ); error:no_release_configured -> @@ -235,12 +240,7 @@ copy_dist_spec(RState, CopyDest, Force) -> }}. bundle_dist_spec(RState, RelName, RelVsn, Force) -> - Config = rebar3_grisp_util:config(RState), - Board = rebar3_grisp_util:platform(Config), - BundleDir = rebar3_grisp_util:deploy_dir(RState), - BundleName = iolist_to_binary(io_lib:format("~s.~s.~s.tar.gz", - [Board, RelName, RelVsn])), - BundleFile = filename:join(BundleDir, BundleName), + BundleFile = rebar3_grisp_util:bundle_file_path(RState, RelName, RelVsn), {bundle, #{ type => archive, force => Force, @@ -300,25 +300,25 @@ event([deploy, distribute, bundle, files, {init, _Dest}]) -> event([deploy, distribute, _Name, files, {_, #{app := App, target := File}}]) -> io:format(" [~p] ~s~n", [App, File]); event([deploy, distribute, bundle, archive, {closed, Path}]) -> - RelPath = grisp_tools_util:make_relative(Path), + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), console("* GRiSP deploy bundle archived in ~s", [RelPath]); event([deploy, distribute, _Name, {error, dir_missing, Path}]) -> - RelPath = grisp_tools_util:make_relative(Path), + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), abort("Missing directory: ~s", [RelPath]); event([deploy, distribute, _Name, {error, dir_access, Path}]) -> - RelPath = grisp_tools_util:make_relative(Path), + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), abort("Directory not accessible: ~s", [RelPath]); event([deploy, distribute, _Name, {error, not_a_directory, Path}]) -> - RelPath = grisp_tools_util:make_relative(Path), + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), abort("Not a proper directory: ~s", [RelPath]); event([deploy, distribute, _Name, {error, file_access, Path}]) -> - RelPath = grisp_tools_util:make_relative(Path), + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), abort("File not accessible: ~s", [RelPath]); event([deploy, distribute, _Name, {error, not_a_file, Path}]) -> - RelPath = grisp_tools_util:make_relative(Path), + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), abort("Not a proper file: ~s", [RelPath]); event([deploy, distribute, _Name, {error, file_exists, Path}]) -> - RelPath = grisp_tools_util:make_relative(Path), + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), abort( "Destination ~s already exists (use --force to overwrite)", [RelPath] @@ -374,48 +374,6 @@ release_handler(#{name := Name, version := Version, erts := Root}, RState) -> % Utility functions -select_release(Args, RState) -> - Relx = rebar_state:get(RState, relx, []), - - Releases = [element(2, R) || R <- Relx, element(1, R) == 'release'], - [error(no_release_configured) || length(Releases) == 0], - - RelName = list_to_atom(proplists:get_value(relname, Args, "undefined")), - RelVsn = proplists:get_value(relvsn, Args), - Indexed = index_releases(Releases), - - case {{RelName, RelVsn}, lists:keyfind(RelName, 1, Indexed)} of - {{undefined, _}, _} when length(Indexed) > 1 -> - error({release_not_selected, Indexed}); - {{undefined, undefined}, _} when length(Indexed) == 1 -> - [{Name, [Version|_]}] = Indexed, - {Name, Version}; - {{RelName, undefined}, {RelName, [Version|_]}} -> - {RelName, Version}; - {{RelName, RelVsn} = Release, {RelName, Versions}} -> - case lists:member(RelVsn, Versions) of - true -> {RelName, RelVsn}; - false -> error({unknown_release_version, Release, Versions}) - end; - {Release, false} -> - error({unknown_release_name, Release, lists:map(fun({N, _}) -> N end, Indexed)}) - end. - -index_releases(Releases) -> - Index = lists:foldl(fun({Name, Version}, Acc) -> - Versions = proplists:get_value(Name, Acc, []), - lists:keystore(Name, 1, Acc, {Name, [Version|Versions]}) - % maps:update_with(Name, fun(L) -> [Version|L] end, [Version], Acc) - end, [], Releases), - lists:map(fun({Name, Versions}) -> - {Name, lists:usort(fun(V1, V2) -> - rlx_util:parsed_vsn_lte( - rlx_util:parse_vsn(V2), % Highest version first - rlx_util:parse_vsn(V1) - ) - end, Versions)} - end, Index). - rel_args(Name, Version, Args) -> RelArgs = case lists:splitwith(fun("--") -> false; (_) -> true end, Args) of {_, ["--"|Rest]} -> Rest; @@ -423,11 +381,6 @@ rel_args(Name, Version, Args) -> end, ["-n", Name, "-v", Version|RelArgs] -- ["-h", "--help", "--version"]. -get_option(Arg, ConfigKey, State) -> - get_arg_option(Arg, State, fun(Config) -> - rebar3_grisp_util:get(ConfigKey, Config) - end). - get_option(Arg, ConfigKey, State, Default) -> get_arg_option(Arg, State, fun(Config) -> rebar3_grisp_util:get(ConfigKey, Config, Default) diff --git a/src/rebar3_grisp_firmware.erl b/src/rebar3_grisp_firmware.erl new file mode 100644 index 0000000..525fb69 --- /dev/null +++ b/src/rebar3_grisp_firmware.erl @@ -0,0 +1,433 @@ +-module(rebar3_grisp_firmware). + +% 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, firmware}, + {module, ?MODULE}, + {bare, true}, + {deps, [{default, install_deps}, {default, compile}]}, + {example, "rebar3 grisp firmware"}, + {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"}, + {bundle, undefined, "bundle", string, "The release bundle to use in the firmware"}, + {force_bundle, $F, "force-bundle", {boolean, false}, "Force bundle deploy even if it already exists"}, + {force, $f, "force", {boolean, false}, "Replace existing files"}, + {compress, $z, "compress", {boolean, true}, "Compress the output files"}, + {system, $s, "system", {boolean, true}, "Generate a system firmware under _grisp/firmware"}, + {image, $i, "image", {boolean, false}, "Generate an eMMC image firmware under _grisp/firmware"}, + {bootloader, $b, "bootloader", {boolean, false}, "Generate a bootloader firmware under _grisp/firmware"}, + {truncate, $t, "truncate", {boolean, true}, "Truncate the generated image firmware to contain only the first partition"}, + {quiet, $q, "quiet", {boolean, false}, "Do not show the instructions on how to burn the firmwares"} + ]}, + {profiles, [grisp]}, + {short_desc, "Generate GRiSP firmware image files"}, + {desc, + "Generate GRiSP complete firmware image or/and system firmware " + "compatible with GRiSP A/B updates.\n" + "\n" + "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" + } + ]), + {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), + case [proplists:get_value(N, Args) || N <- [bootloader, image, system]] of + [false, false, false] -> erlang:error(no_firmware_to_be_extracted); + _ -> ok + end, + 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(bundle, Args, undefined) of + undefined -> + Force = proplists:get_value(force_bundle, Args, false), + case get_bundle(RState, Force, RelName, RelVsn, ExtraArgs) of + {error, _Reason} = Error -> Error; + {ok, BundleFile, RState2} -> + grisp_tools_firmware(RState2, RelName, RelVsn, + [{bundle, BundleFile} | Args]) + end; + BundleFile -> + case filelib:is_file(BundleFile) of + false -> + abort("Bundle file not found: ~s", [BundleFile]); + true -> + RelPath = grisp_tools_util:maybe_relative(BundleFile, ?MAX_DDOT), + console("* Using provided bundle: ~s", [RelPath]), + grisp_tools_firmware(RState, RelName, RelVsn, Args) + end + end + catch + error:no_firmware_to_be_extracted -> + abort( + "No firmware selected.~n" + "You must either select image ot boottloader firmware with options~n" + "-i/--image or -b/--boot, or not disable system firmware.~n", + [] + ); + 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 firmware --relname ~p~n" + " rebar3 grisp firmware --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 firmware.~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 ------------------------------------------------------------------ + +toolchain_root(RebarState) -> + case rebar3_grisp_util:toolchain_root(RebarState) of + undefined -> undefined; + {docker, _DockerImage} = Result -> Result; + {directory, _Directory} = Result -> Result; + {error, docker_not_found} -> abort("Docker is not available") + end. + +get_bundle(RState, Force, RelName, RelVsn, ExtraRelArgs) -> + BundleFile = rebar3_grisp_util:bundle_file_path(RState, RelName, RelVsn), + case filelib:is_file(BundleFile) of + true when Force =:= false -> + RelPath = grisp_tools_util:maybe_relative(BundleFile, ?MAX_DDOT), + console("* Using existing bundle: ~s", [RelPath]), + {ok, BundleFile, RState}; + _ -> + console("* Deploying bundle...", []), + case deploy_bundle(RState, Force, RelName, + RelVsn, ExtraRelArgs) of + {ok, RState2} -> {ok, BundleFile, RState2}; + {error, _Reason} = Error -> Error + end + end. + +deploy_bundle(RState, Force, RelName, RelVsn, ExtraRelArgs) -> + Args = [ + "--tar", + "--relname", atom_to_list(RelName), + "--relvsn", RelVsn, + "--destination", "" + ] ++ case Force =:= true of + true -> ["--force"]; + false -> [] + end ++ case ExtraRelArgs of + [_|_] -> ["--" | ExtraRelArgs]; + _ -> [] + end, + rebar3_grisp_util:rebar_command(RState, grisp, deploy, Args). + +grisp_tools_firmware(RState, RelName, RelVsn, Args) -> + Config = rebar3_grisp_util:config(RState), + Board = rebar3_grisp_util:platform(Config), + BundleOpts = proplists:get_value(bundle, Args), + ForceOpts = proplists:get_value(force, Args, false), + GenSystemOpts = proplists:get_value(system, Args, true), + GenImageOpts = proplists:get_value(image, Args, false), + GenBootOpts = proplists:get_value(bootloader, Args, false), + TruncateOpts = proplists:get_value(truncate, Args, true), + CompressOpts = proplists:get_value(compress, Args, true), + ToolchainRoot = toolchain_root(RState), + case {ToolchainRoot, GenImageOpts, GenBootOpts} of + {undefined, A, B} when A =:= true; B =:= true -> + abort("Cannot generate image or bootloader firmware without a valid toolchain"); + _ -> ok + end, + SystemSpec = case GenSystemOpts of + false -> undefined; + true -> #{ + compress => CompressOpts, + target => rebar3_grisp_util:firmware_file_path(RState, system, + RelName, RelVsn) + } + end, + ImageSpec = case GenImageOpts of + false -> undefined; + true -> #{ + compress => CompressOpts, + truncate => TruncateOpts, + target => rebar3_grisp_util:firmware_file_path(RState, image, + RelName, RelVsn) + } + end, + BootSpec = case GenBootOpts of + false -> undefined; + true -> #{ + compress => CompressOpts, + target => rebar3_grisp_util:firmware_file_path(RState, boot, + RelName, RelVsn) + } + end, + FirmwareSpec = #{ + platform => Board, + force => ForceOpts, + toolchain => ToolchainRoot, + bundle => BundleOpts, + system => SystemSpec, + image => ImageSpec, + boot => BootSpec, + handlers => grisp_tools:handlers_init(#{ + event => {fun event_handler/2, RState}, + shell => {fun rebar3_grisp_handler:shell/3, #{}} + }) + }, + State = grisp_tools:firmware(FirmwareSpec), + #{event := RState2} = grisp_tools:handlers_finalize(State), + info("Firmware(s) created"), + case proplists:get_value(quiet, Args) of + false -> info("~s", [firmware_usage(State)]); + true -> ok + end, + {ok, RState2}. + +event_handler(Event, RState) -> + event(Event), + {ok, RState}. + +event([firmware, prepare]) -> + console("* Preparing and validating..."); +event([firmware, prepare, _, {bootloader, Name}]) -> + console(" Bootloader selected: ~s", [Name]); +event([firmware, prepare, _, {error, unsupported_platform, Platform}]) -> + abort("Platform ~s not supported", [Platform]); +event([firmware, prepare, _, {error, bundle_not_found, Path}]) -> + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), + abort("Bundle file not found: ~s", [RelPath]); +event([firmware, prepare, _, {error, toolchain_not_found, {directory, Path}}]) -> + abort("Toolchain root directory not found: ~s", [Path]); +event([firmware, prepare, _, {error, toolchain_not_found, {docker, ImageName}}]) -> + abort("Toolchain docker image not found: ~s", [ImageName]); +event([firmware, prepare, _, {error, toolchain_required}]) -> + abort("A toolchain is required to generate image and/or bootloader firmwares", []); +event([firmware, prepare, _, {error, docker_error, Reason}]) -> + abort("Docker error: ~p", [Reason]); +event([firmware, prepare, _, {error, directory_not_found, Path}]) -> + abort("Expected directory not found: ~p", [Path]); +event([firmware, 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([firmware, prepare, _, {error, file_access, Path}]) -> + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), + abort("File not accessible: ~s", [RelPath]); +event([firmware, prepare, _, {error, not_a_file, Path}]) -> + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), + abort("Not a regular file: ~s", [RelPath]); +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); +event([firmware, build_firmware, copy_bootloader]) -> + console("* Writing bootloader..."); +event([firmware, build_firmware, copy_bootloader, {error, Reason}]) -> + abort_message("Failed to write bootloader", Reason); +event([firmware, build_firmware, create_partitions]) -> + console("* Creating disk partition table..."); +event([firmware, build_firmware, create_partitions, {error, Reason}]) -> + abort_message("Failed to create partition table", Reason); +event([firmware, build_firmware, format_system]) -> + console("* Formatting system partitions..."); +event([firmware, build_firmware, format_system, {error, Reason}]) -> + abort_message("Failed to format system partition", Reason); +event([firmware, build_firmware, deploy_bundle]) -> + console("* Deploying release bundle..."); +event([firmware, build_firmware, deploy_bundle, {error, Reason}]) -> + abort_message("Failed to expand release bundle", Reason); +event([firmware, build_firmware, extract_system]) -> + console("* Extracting system firmware..."); +event([firmware, build_firmware, extract_system, {extracted, Path}]) -> + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), + console(" System firmware exported: ~s", [RelPath]); +event([firmware, build_firmware, extract_system, {error, Reason}]) -> + abort_message("Failed to extract system firmware", Reason); +event([firmware, build_firmware, extract_image]) -> + console("* Extracting image firmware..."); +event([firmware, build_firmware, extract_image, {extracted, Path}]) -> + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), + console(" eMMC image firmware exported: ~s", [RelPath]); +event([firmware, build_firmware, extract_image, {error, Reason}]) -> + abort_message("Failed to extract eMMC image firmware", Reason); +event([firmware, build_firmware, extract_boot]) -> + console("* Extracting bootloader firmware..."); +event([firmware, build_firmware, extract_boot, {extracted, Path}]) -> + RelPath = grisp_tools_util:maybe_relative(Path, ?MAX_DDOT), + console(" Bootloader firmware exported: ~s", [RelPath]); +event([firmware, build_firmware, extract_boot, {error, Reason}]) -> + abort_message("Failed to extract bootloader firmware", Reason); +event([firmware, build_firmware, close_image]) -> + console("* Cleaning up..."); +event([firmware, build_firmware, close_image, {error, Reason}]) -> + abort_message("Failed to cleanup", Reason); +event([firmware, build_firmware, _, {exec, Args}]) -> + LogLine = iolist_to_binary(lists:join(" ", Args)), + debug("command: ~s", [LogLine]); +event([firmware, build_firmware, _, {exit, {status, Status}}]) -> + debug("[exit ~w]", [Status]); +event([firmware, build_firmware, _, {exit, {signal, Sig}}]) -> + debug("[signal ~w]", [Sig]); +event([firmware, build_firmware, _, {Stream, eof}]) + when Stream =:= stdin; Stream =:= stdout; Stream =:= stderr -> + debug("[~s closed]", [Stream]); +event([firmware, build_firmware, _, {Stream, Data}]) + when Stream =:= stdin; Stream =:= stdout; Stream =:= stderr -> + Str = unicode:characters_to_list(Data), + debug("~s ~s", [stream_tag(Stream), string:strip(Str, right, $\n)]); +event([firmware, build_firmware, _, {Tag, _Term}]) + when Tag =:= result; Tag =:= error -> + ok; +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]); + {error, Reason, Info} -> + abort("Unexpected 2 ~p error: ~p", [Reason, Info]); + {error, Reason} -> + abort("Unexpected ~p error", [Reason]); + _ -> ok + end. + +stream_tag(stdin) -> "<<"; +stream_tag(stdout) -> "1>"; +stream_tag(stderr) -> "2>". + +abort_message(Prefix, Msg) when is_binary(Msg) -> + abort("~s; ~s", [Prefix, Msg]); +abort_message(Prefix, Reason) -> + abort("~s: ~p", [Prefix, Reason]). + +firmware_usage(FirmwareSpec) -> + FirmwareItems = maps:to_list(maps:with([system, image, boot], FirmwareSpec)), + FirmwareDesc = #{ + system => {"system", "/dev/mmc1.0"}, + image => {"emmc", "/dev/mmc1"}, + boot => {"bootloader", "/dev/mmc1"} + }, + Firmwares = [{K, R, L, D} || {K, #{target := T}} <- FirmwareItems, + {L, D} <- [maps:get(K, FirmwareDesc)], + R <- [grisp_tools_util:maybe_relative(T, ?MAX_DDOT)]], + Prefix = " ", + Cautions = iolist_to_binary([ + case FirmwareSpec of + #{image := #{truncate := true}} -> + [[Prefix, "- When writing a truncated eMMC image firmware, only the first\n"], + [Prefix, " system partition is written. If the active system is the\n"], + [Prefix, " second one, the board will continue to boot the old software.\n"], + [Prefix, " You will need to manually change the active system partition\n"], + [Prefix, " in the bootloader console and restart the board:\n"], + [Prefix, " $ let state.bootstate.active_system=0\n"], + [Prefix, " $ state -s\n"]]; + _ -> [] + end, + case FirmwareSpec of + #{system := #{}} -> + [[Prefix, "- When writing a system firmware, be sure to do it on the active system\n"], + [Prefix, " partition (/dev/mmc1.0 or /dev/mmc1.1) or the board will continue to\n"], + [Prefix, " boot the old software.\n"]]; + _ -> [] + end + ]), + iolist_to_binary([ + ["Instructions to update GRiSP2 firmware(s):\n"], + case os:type() of + {unix, darwin} -> + [[Prefix, "- Copy the relevent firmware(s) to the SD card:\n"], + [[Prefix, " $ cp \"", R, "\" /Volumes/GRISP # ", L, " firmware\n"] + || {_, R, L, _} <- Firmwares], + [Prefix, "- Unmount the SD card:\n"], + [Prefix, " $ diskutil umount /Volumes/GRISP\n"], + [Prefix, "- Open a serial console to the GRiSP board:\n"], + [Prefix, " $ screen /dev/tty.usbserial-0${GRISP_BOARD_SERIAL}1 115200\n"]]; + {unix, linux} -> + [[Prefix, "- Copy the relevent firmware(s) to the SD card:\n"], + [[Prefix, " $ cp \"", R, "\" /media/$USER/GRISP # ", L, " firmware\n"] + || {_, R, L, _} <- Firmwares], + [Prefix, "- Unmount the SD card:\n"], + [Prefix, " $ umount /media/$USER/GRISP\n"], + [Prefix, "- Open a serial console to the GRiSP board:\n"], + [Prefix, " $ screen /dev/ttyUSB1 115200\n"]]; + _ -> + [[Prefix, "- Copy the relevent firmware(s) to the SD card.\n"], + [Prefix, "- Unmount the SD card.\n"], + [Prefix, "- Open a serial console to the GRiSP board.\n"]] + end, + [Prefix, "- Insert the SD card.\n"], + [Prefix, "- Reset the GRiSP board using the onboard reset button.\n"], + [Prefix, "- Enter into barebox console mode by pressing any key before 3 seconds.\n"], + [Prefix, "- Write the relevent firmware(s):\n"], + [[Prefix, " $ uncompress \"/mnt/mmc/", filename:basename(R), "\" ", D, " # ", L, " firmware\n"] + || {_, R, L, D} <- Firmwares], + [Prefix, "- Remove the SD card.\n"], + [Prefix, "- Reset the GRiSP board again.\n"], + case Cautions of + <<>> -> []; + _ -> ["CAUTIONS:\n", Cautions] + end + ]). diff --git a/src/rebar3_grisp_util.erl b/src/rebar3_grisp_util.erl index 6733dc1..3c2dcd3 100644 --- a/src/rebar3_grisp_util.erl +++ b/src/rebar3_grisp_util.erl @@ -32,6 +32,14 @@ -export([merge_config/2]). -export([should_build/1]). -export([ensure_dir/1]). +-export([select_release/3]). +-export([bundle_file_name/3]). +-export([bundle_file_path/3]). +-export([rebar_command/4]). +-export([firmware_dir/1]). +-export([firmware_file_name/4]). +-export([firmware_file_path/4]). +-export([toolchain_root/1]). -import(rebar3_grisp_tools, [event/2]). @@ -153,6 +161,110 @@ ensure_dir(File) -> Error -> abort("Could not create target directory: ~p", [Error]) end. +select_release(RebarState, RelName0, RelVsn) + when is_atom(RelName0) orelse is_list(RelName0), + RelVsn =:= undefined orelse is_list(RelVsn) -> + RelName = case is_list(RelName0) of + true -> list_to_atom(RelName0); + false -> RelName0 + end, + Relx = rebar_state:get(RebarState, relx, []), + Releases = [element(2, R) || R <- Relx, element(1, R) == 'release'], + [erlang:error(no_release_configured) || length(Releases) == 0], + Indexed = index_releases(Releases), + + case {{RelName, RelVsn}, lists:keyfind(RelName, 1, Indexed)} of + {{undefined, _}, _} when length(Indexed) > 1 -> + erlang:error({release_not_selected, Indexed}); + {{undefined, undefined}, _} when length(Indexed) == 1 -> + [{Name, [Version|_]}] = Indexed, + {Name, Version}; + {{RelName, undefined}, {RelName, [Version|_]}} -> + {RelName, Version}; + {{RelName, RelVsn} = Release, {RelName, Versions}} -> + case lists:member(RelVsn, Versions) of + true -> {RelName, RelVsn}; + false -> + erlang:error({unknown_release_version, Release, Versions}) + end; + {Release, false} -> + erlang:error({unknown_release_name, Release, + lists:map(fun({N, _}) -> N end, Indexed)}) + end. + +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])). + +bundle_file_path(RebarState, RelName, RelVsn) -> + BundleDir = rebar3_grisp_util:deploy_dir(RebarState), + BundleName = bundle_file_name(RebarState, RelName, RelVsn), + filename:join(BundleDir, BundleName). + +firmware_dir(RebarState) -> + 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])); +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])); +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])). + +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). + +rebar_command(RebarState, Namespace, Command, Args) -> + % Backup current command state + OriginalNamespace = rebar_state:namespace(RebarState), + OriginalArgs = rebar_state:command_args(RebarState), + OriginalParsedArgs = rebar_state:command_parsed_args(RebarState), + + % Args are parsed by rebar_core + RebarState2 = rebar_state:namespace(RebarState, Namespace), + RebarState3 = rebar_state:command_args(RebarState2, Args), + + case rebar_core:process_command(RebarState3, Command) of + {error, _Reason} = Error -> Error; + {ok, RS} -> + % Restore current command state + RS2 = rebar_state:namespace(RS, OriginalNamespace), + RS3 = rebar_state:command_args(RS2, OriginalArgs), + RS4 = rebar_state:command_parsed_args(RS3, OriginalParsedArgs), + {ok, RS4} + end. + +toolchain_root(RebarState) -> + Config = rebar3_grisp_util:config(RebarState), + DockerImg = rebar3_grisp_util:get([build, toolchain, docker], Config, error), + TCDir = rebar3_grisp_util:get([build, toolchain, directory], Config, error), + case os:getenv("GRISP_TOOLCHAIN", TCDir) of + error -> case DockerImg of + error -> undefined; + DockerImage -> + case rebar3_grisp_util:sh("docker info",[return_on_error]) of + {error, _} -> {error, docker_not_found}; + {ok, _} -> {docker, DockerImage} + end + end; + Directory -> + {directory, Directory} + end. + + %--- Internal ------------------------------------------------------------------ deep_get([], Value, _Default) -> @@ -180,3 +292,18 @@ merge_config_([{Key, Val}, {Key, _} | Rest], Acc) -> merge_config_(Rest, [{Key, Val} | Acc]); merge_config_([Item | Rest], Acc) -> merge_config_(Rest, [Item | Acc]). + +index_releases(Releases) -> + Index = lists:foldl(fun({Name, Version}, Acc) -> + Versions = proplists:get_value(Name, Acc, []), + lists:keystore(Name, 1, Acc, {Name, [Version|Versions]}) + % maps:update_with(Name, fun(L) -> [Version|L] end, [Version], Acc) + end, [], Releases), + lists:map(fun({Name, Versions}) -> + {Name, lists:usort(fun(V1, V2) -> + rlx_util:parsed_vsn_lte( + rlx_util:parse_vsn(V2), % Highest version first + rlx_util:parse_vsn(V1) + ) + end, Versions)} + end, Index).