Skip to content

Commit

Permalink
Allow for a specific (non-checksummed) ShellCheck version to be used (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
paulo-ferraz-oliveira authored Aug 11, 2023
1 parent 674ef7a commit 77b5332
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 67 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ MD5-based checksum (using `crypto`) against the files we're about to execute. If
don't coincide, the execution is aborted. You can override this by setting option
`{checkshell, [{checksum, false}]}.` in `rebar.config`.

#### Choosing a ShellCheck version

The plugin ships with its own rules for downloading, caching and checksumming a specific ShellCheck
**version**. You can override this by setting option `{checkshell, [{vsn, "v0.9.0"}]}.` in
`rebar.config`, in which case the plugin will warn you that the check is turned off.

**Note**: using a ShellCheck version different from the one the plugin targets by default may
introduce unexpected error (also, new versions might bring e.g. new options that won't be available
for consumption out-of-the-box). In this case, feel free to pull request, or open a GitHub issue
to discuss moving forward.

## The project

### Changelog
Expand Down
3 changes: 2 additions & 1 deletion src/rebar3_checkshell.app.src
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
{applications, [
% OTP dependencies
crypto,
inets,
kernel,
stdlib,
sasl,
stdlib,
% External dependencies
tls_certificate_check
]},
Expand Down
1 change: 1 addition & 0 deletions src/rebar3_checkshell.erl
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ init(State) ->
Files :: [string()],
Result :: ok.
main(Files) ->
{ok, _Started} = application:ensure_all_started(rebar3_checkshell),
_ = rebar3_checkshell_prv:do_for(Files, rebar_state:new()),
ok.
8 changes: 4 additions & 4 deletions src/rebar3_checkshell_arch.erl
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ do(Files, State) ->
do({error, E} = _InstallRes, _File, _State) ->
{error, "checkshell: installation returned " ++ E};
do(ok = _InstallRes, Files, State) ->
Cmd = rebar3_checkshell_inst:shellcheck_path(),
Cmd = rebar3_checkshell_inst:shellcheck_path(State),
Args = args(Files, State),
result(rebar3_checkshell_utils:cmd(Cmd, Args), State).

Expand Down Expand Up @@ -69,7 +69,7 @@ args(Files, State) ->
OptsFromRebarConfig
),
MaybeColor = rebar3_checkshell_prv:opt(State, color, undefined),
OptsForShellCheck ++ [maybe_colorize(MaybeColor)] ++ [opt({files, Files})].
OptsForShellCheck ++ [maybe_colorize(MaybeColor)] ++ opt({files, Files}).

-spec maybe_colorize(Color) -> Result when
Color :: undefined | auto | always | never,
Expand All @@ -81,7 +81,7 @@ maybe_colorize(_Color) ->

-spec opt(Option) -> Result when
Option :: atom() | {atom(), atom() | list() | string() | integer()},
Result :: string().
Result :: string() | list().
opt(check_sourced) ->
"--check-sourced";
opt({color, Color}) when Color =:= auto orelse Color =:= always orelse Color =:= never ->
Expand Down Expand Up @@ -177,7 +177,7 @@ opt({wiki_link_count, Num}) when is_integer(Num) andalso Num > 0 ->
opt(external_sources) ->
"--external-sources";
opt({files, Files}) when is_list(Files) ->
"" ++ Files;
Files;
opt(UnknownOption) ->
_ = rebar_log:log(
warn,
Expand Down
148 changes: 89 additions & 59 deletions src/rebar3_checkshell_inst.erl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
-include("rebar3_checkshell.hrl").

-export([put_executables/1]).
-export([shellcheck_path/0]).
-export([shellcheck_path/1]).

-define(SHELLCHECK_VERSION, "v0.9.0").
-elvis([{elvis_style, operator_spaces, disable}]).
Expand All @@ -27,22 +27,22 @@ put_executables(State) ->
ArchCacheDirExists = filelib:is_dir(arch_cache_dir()),
CacheDirResult = mkdir_arch_cache(ArchCacheDirExists),

VsnCacheDirExists = filelib:is_dir(vsn_cache_dir()),
VsnDirResult = mkdir_vsn_cache(VsnCacheDirExists, CacheDirResult),
VsnCacheDirExists = filelib:is_dir(vsn_cache_dir(State)),
VsnDirResult = mkdir_vsn_cache(VsnCacheDirExists, CacheDirResult, State),

CompressedTargetExists = filelib:is_file(compressed_target()),
DownloadAndWriteResult = download_and_write(CompressedTargetExists, VsnDirResult),
CompressedTargetExists = filelib:is_file(compressed_target(State)),
DownloadAndWriteResult = download_and_write(CompressedTargetExists, VsnDirResult, State),

ExpandedExists = filelib:is_file(shellcheck_path()),
ExpandResult = expand(ExpandedExists, DownloadAndWriteResult),
ExpandedExists = filelib:is_file(shellcheck_path(State)),
ExpandResult = expand(ExpandedExists, DownloadAndWriteResult, State),
CheckSummed = rebar3_checkshell_prv:opt(State, checksum, should_checksum(State)),
checksum(CheckSummed, ExpandResult, State).

CheckSummed = rebar3_checkshell_prv:opt(State, checksum, true),
checksum(CheckSummed, ExpandResult).

-spec compressed_target() -> Result when
-spec compressed_target(State) -> Result when
State :: rebar_state:t(),
Result :: string().
compressed_target() ->
filename:join(vsn_cache_dir(), installer_name()).
compressed_target(State) ->
filename:join(vsn_cache_dir(State), installer_name(State)).

% supertype
-dialyzer({nowarn_function, arch_folder_name/0}).
Expand Down Expand Up @@ -78,15 +78,26 @@ global_cache_dir() ->
arch_cache_dir() ->
filename:join(global_cache_dir(), checkshell_arch_folder_name()).

-spec vsn_cache_dir() -> Result when
-spec vsn_cache_dir(State) -> Result when
State :: rebar_state:t(),
Result :: string().
vsn_cache_dir() ->
filename:join(arch_cache_dir(), ?SHELLCHECK_VERSION).
vsn_cache_dir(State) ->
{_IsDefaultVsn, Vsn} = vsn(State),
filename:join(arch_cache_dir(), Vsn).

-spec vsn(State) -> Result when
State :: rebar_state:t(),
Result :: {IsDefaultVsn :: boolean(), Vsn :: string()}.
vsn(State) ->
DefaultVsn = ?SHELLCHECK_VERSION,
Vsn = rebar3_checkshell_prv:opt(State, vsn, DefaultVsn),
{Vsn =:= DefaultVsn, Vsn}.

-spec shellcheck_path() -> Result when
-spec shellcheck_path(State) -> Result when
State :: rebar_state:t(),
Result :: string().
shellcheck_path() ->
filename:join(vsn_cache_dir(), executable()).
shellcheck_path(State) ->
filename:join(vsn_cache_dir(State), executable()).

% supertype
-dialyzer({nowarn_function, executable/0}).
Expand Down Expand Up @@ -121,40 +132,44 @@ mkdir_arch_cache(false = _Exists) ->
]),
filelib:ensure_path(ArchCacheDir).

-spec mkdir_vsn_cache(Exists, CacheDirResult) -> Result when
-spec mkdir_vsn_cache(Exists, CacheDirResult, State) -> Result when
Exists :: boolean(),
CacheDirResult :: ok | {error, file:posix()},
State :: rebar_state:t(),
Result :: ok | {error, file:posix()}.
mkdir_vsn_cache(true = _Exists, _CacheDirResult) ->
mkdir_vsn_cache(true = _Exists, _CacheDirResult, _State) ->
_ = rebar_log:log(debug, "checkshell: vsn cache dir exists", []),
ok;
mkdir_vsn_cache(false = _Exists, {error, _FilePosix} = CacheDirResult) ->
mkdir_vsn_cache(false = _Exists, {error, _FilePosix} = CacheDirResult, _State) ->
_ = rebar_log:log(debug, "checkshell: (vsn cache) prior error ~p", [CacheDirResult]),
CacheDirResult;
mkdir_vsn_cache(false = _Exists, ok = _CacheDirResult) ->
VsnCacheDir = vsn_cache_dir(),
mkdir_vsn_cache(false = _Exists, ok = _CacheDirResult, State) ->
VsnCacheDir = vsn_cache_dir(State),
_ = rebar_log:log(info, "checkshell: creating cache/version dir at ~p", [VsnCacheDir]),
filelib:ensure_path(VsnCacheDir).

-spec download_url() -> Result when
-spec download_url(State) -> Result when
State :: rebar_state:t(),
Result :: nonempty_ubytes().
download_url() ->
"https://github.com/koalaman/shellcheck/releases/download/" ++ ?SHELLCHECK_VERSION ++ "/" ++
installer_name().
download_url(State) ->
{_IsDefaultVsn, Vsn} = vsn(State),
"https://github.com/koalaman/shellcheck/releases/download/" ++ Vsn ++ "/" ++
installer_name(State).

-spec download_and_write(CompressedTargetExists, VsnDirResult) -> Result when
-spec download_and_write(CompressedTargetExists, VsnDirResult, State) -> Result when
CompressedTargetExists :: boolean(),
VsnDirResult :: ok | {error, file:posix()},
State :: rebar_state:t(),
Result :: ok | {error, file:posix()}.
download_and_write(true = _CompressedTargetExists, _VsnDirResult) ->
download_and_write(true = _CompressedTargetExists, _VsnDirResult, _State) ->
_ = rebar_log:log(debug, "checkshell: compressed target exists", []),
ok;
download_and_write(false = _CompressedTargetExists, {error, _FilePosix} = VsnDirResult) ->
download_and_write(false = _CompressedTargetExists, {error, _FilePosix} = VsnDirResult, _State) ->
_ = rebar_log:log(debug, "checkshell: (download and write) prior error ~p", [VsnDirResult]),
VsnDirResult;
download_and_write(false = _CompressedTargetExists, ok = _VsnDirResult) ->
URL = download_url(),
VsnCacheDir = vsn_cache_dir(),
download_and_write(false = _CompressedTargetExists, ok = _VsnDirResult, State) ->
URL = download_url(State),
VsnCacheDir = vsn_cache_dir(State),
HttpHeaders = [],
HttpOptions = [{ssl, tls_certificate_check:options(URL)}],
Options = [{body_format, binary}],
Expand All @@ -163,21 +178,29 @@ download_and_write(false = _CompressedTargetExists, ok = _VsnDirResult) ->
get, {URL, HttpHeaders}, HttpOptions, Options
),

CompressedTarget = compressed_target(),
CompressedTarget = compressed_target(State),
ok = file:write_file(CompressedTarget, HttpBodyResult).

-spec checksum(CheckSummed, ExpandResult) -> Result when
-spec should_checksum(State) -> Result when
State :: rebar_state:t(),
Result :: boolean().
should_checksum(State) ->
{IsDefaultVsn, _Vsn} = vsn(State),
IsDefaultVsn.

-spec checksum(CheckSummed, ExpandResult, State) -> Result when
CheckSummed :: boolean(),
ExpandResult :: ok | {error, file:posix()},
State :: rebar_state:t(),
Result :: ok | {error, nonempty_ubytes()}.
checksum(false = _CheckSummed, _ExpandResult) ->
checksum(false = _CheckSummed, _ExpandResult, _State) ->
_ = rebar_log:log(warn, "checkshell: checksum bypass is ON", []),
ok;
checksum(true = _CheckSummed, {error, FilePosix} = ExpandResult) ->
checksum(true = _CheckSummed, {error, FilePosix} = ExpandResult, _State) ->
_ = rebar_log:log(debug, "checkshell: (expand for) prior error ~p", [ExpandResult]),
{error, "(check with DEBUG=1) " ++ atom_to_list(FilePosix)};
checksum(true = _CheckSummed, ok = _ExpandResult) ->
{ok, ShellCheck} = file:read_file(shellcheck_path()),
checksum(true = _CheckSummed, ok = _ExpandResult, State) ->
{ok, ShellCheck} = file:read_file(shellcheck_path(State)),
do_checksum(rebar3_checkshell_arch:t(), crypto:hash(md5, ShellCheck)).

-spec do_checksum(Arch, Checksum) -> Result when
Expand All @@ -194,27 +217,29 @@ do_checksum(Arch, Checksum) when
do_checksum(_Arch, _Expected) ->
{error, "invalid executable checksum"}.

-spec expand(ExpandedExists, DownloadAndWriteResult) -> Result when
-spec expand(ExpandedExists, DownloadAndWriteResult, State) -> Result when
ExpandedExists :: boolean(),
DownloadAndWriteResult :: ok | {error, file:posix()},
State :: rebar_state:t(),
Result :: ok | {error, file:posix()}.
expand(ExpandedExists, DownloadAndWriteResult) ->
expand_for(ExpandedExists, DownloadAndWriteResult).
expand(ExpandedExists, DownloadAndWriteResult, State) ->
expand_for(ExpandedExists, DownloadAndWriteResult, State).

-spec expand_for(ExpandedExists, DownloadAndWriteResult) -> Result when
-spec expand_for(ExpandedExists, DownloadAndWriteResult, State) -> Result when
ExpandedExists :: boolean(),
DownloadAndWriteResult :: ok | {error, file:posix()},
State :: rebar_state:t(),
Result :: ok | {error, file:posix()}.
expand_for(true = _ExpandedExists, _DownloadAndWriteResult) ->
expand_for(true = _ExpandedExists, _DownloadAndWriteResult, _State) ->
_ = rebar_log:log(debug, "checkshell: expanded target exists", []),
ok;
expand_for(false = _ExpandedExists, {error, _Result} = DownloadAndWriteResult) ->
expand_for(false = _ExpandedExists, {error, _Result} = DownloadAndWriteResult, _State) ->
_ = rebar_log:log(debug, "checkshell: (expand for) prior error ~p", [DownloadAndWriteResult]),
DownloadAndWriteResult;
expand_for(false = _ExpandedExists, ok = _DownloadAndWriteResult) ->
expand_for(false = _ExpandedExists, ok = _DownloadAndWriteResult, State) ->
FileType = file_type(),
CompressedTarget = compressed_target(),
VsnCacheDir = vsn_cache_dir(),
CompressedTarget = compressed_target(State),
VsnCacheDir = vsn_cache_dir(State),
_ = rebar_log:log(info, "checkshell: extracting executable to ~p", [VsnCacheDir]),
do_expand_for(FileType, CompressedTarget, VsnCacheDir).

Expand Down Expand Up @@ -248,17 +273,22 @@ file_type_for(linux) ->
file_type_for(win32) ->
zip.

-spec installer_name() -> Result when
-spec installer_name(State) -> Result when
State :: rebar_state:t(),
Result :: nonempty_ubytes().
installer_name() ->
installer_name_for(rebar3_checkshell_arch:t()).
installer_name(State) ->
installer_name_for(rebar3_checkshell_arch:t(), State).

-spec installer_name_for(Arch) -> Result when
-spec installer_name_for(Arch, State) -> Result when
State :: rebar_state:t(),
Arch :: rebar3_checkshell_arch:t(),
Result :: nonempty_ubytes().
installer_name_for(darwin) ->
"shellcheck-" ++ ?SHELLCHECK_VERSION ++ ".darwin.x86_64.tar.xz";
installer_name_for(linux) ->
"shellcheck-" ++ ?SHELLCHECK_VERSION ++ ".linux.x86_64.tar.xz";
installer_name_for(win32) ->
"shellcheck-" ++ ?SHELLCHECK_VERSION ++ ".zip".
installer_name_for(darwin, State) ->
{_IsDefaultVsn, Vsn} = vsn(State),
"shellcheck-" ++ Vsn ++ ".darwin.x86_64.tar.xz";
installer_name_for(linux, State) ->
{_IsDefaultVsn, Vsn} = vsn(State),
"shellcheck-" ++ Vsn ++ ".linux.x86_64.tar.xz";
installer_name_for(win32, State) ->
{_IsDefaultVsn, Vsn} = vsn(State),
"shellcheck-" ++ Vsn ++ ".zip".
5 changes: 3 additions & 2 deletions src/rebar3_checkshell_prv.erl
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,13 @@ opt(State, Opt, Default) ->
Result :: [{Abs :: string(), Opt :: string()}].
files_from_cli(State) ->
{Args, _} = rebar_state:command_parsed_args(State),
OptFiles = proplists:get_value(files, Args, []),
OptFiles0 = proplists:get_value(files, Args, []),
OptFiles = string:split(OptFiles0, ",", all),
[{filename:absname(OptFile), OptFile} || OptFile <- OptFiles].

-spec files_from_rebar_config(State) -> Result when
State :: term(),
Result :: [{Abs :: string(), Opt :: string()}].
files_from_rebar_config(State) ->
OptFiles = opt(State, files),
OptFiles = opt(State, files, []),
[{filename:absname(OptFile), OptFile} || OptFile <- OptFiles].
2 changes: 1 addition & 1 deletion src/rebar3_checkshell_utils.erl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Args :: [string()],
Result :: {ExitStatus :: non_neg_integer(), Data :: string()}.
cmd(Cmd, Args0) ->
Args = [" " ++ Arg || Arg <- Args0],
Args = [" " ++ Arg || Arg <- Args0, Arg =/= ""],
PortName = {spawn, Cmd ++ Args},
PortSettings = [exit_status],
Port = open_port(PortName, PortSettings),
Expand Down

0 comments on commit 77b5332

Please sign in to comment.