Files
mercury/compiler/system_cmds.m
Zoltan Somogyi 4e6a6357c4 Use standard but controllable "can't open file" messages.
compiler/file_util.m:
    Define standard routines for reporting that we couldn't open a file
    for either input or output. Rename the one other predicate in this file
    that the compiler used for that purpose, so that its call sites
    can be redirected to use one of the routines intended for this purpose.

    Give these routines the ability to standardize path names (by removing
    "./" prefixes from relative path names, and removing all directory names
    from adsolute path names) if a new option is given.

compiler/options.m:
    Add that (developer-only) option.

compiler/compile_target_code.m:
compiler/copy_util.m:
compiler/export.m:
compiler/llds_out_file.m:
compiler/mercury_compile_front_end.m:
compiler/mercury_compile_make_hlds.m:
compiler/mercury_compile_middle_passes.m:
compiler/mercury_compile_mlds_back_end.m:
compiler/mlds_to_c_file.m:
compiler/mlds_to_cs_file.m:
compiler/mlds_to_java_file.m:
compiler/module_cmds.m:
compiler/opt_deps_spec.m:
compiler/output_imports_graph.m:
compiler/passes_aux.m:
compiler/system_cmds.m:
compiler/write_deps_file.m:
compiler/xml_documentation.m:
    Replace all call sites to the file_util.m predicates modified
    by this diff. In many cases, it replaces duplicate copies of the
    same code with a call. In some cases, redirect the error message
    to the progress stream, where it belongs.

tests/invalid/Mercury.options:
tests/invalid/foreign_include_file_missing.err_exp3:
    Compile the foreign_include_file_missing test case with the new option,
    and add the expected output in C# grades, which (thanks to the option)
    no longer includes the pathname of the test directory.

tests/invalid/foreign_include_file_missing.m:
    Record what the new .err_exp3 file is for.

tests/warnings/help_text.err_exp:
    Expect the new option.
2025-10-11 12:10:26 +11:00

478 lines
19 KiB
Mathematica

%-----------------------------------------------------------------------------%
% vim: ft=mercury ts=4 sw=4 et
%-----------------------------------------------------------------------------%
% Copyright (C) 2008-2012 The University of Melbourne.
% Copyright (C) 2013-2025 The Mercury team.
% This file may only be copied under the terms of the GNU General
% Public License - see the file COPYING in the Mercury distribution.
%-----------------------------------------------------------------------------%
%
% File: system_cmds.m.
%
% This module provides predicates to invoke commands via the shell.
%
%-----------------------------------------------------------------------------%
:- module libs.system_cmds.
:- interface.
:- import_module libs.globals.
:- import_module libs.maybe_util.
:- import_module io.
:- import_module maybe.
%-----------------------------------------------------------------------------%
:- type quote_char
---> forward % '
; double. % "
:- type command_verbosity
---> cmd_verbose
% Output the command line only with `--verbose'.
; cmd_verbose_commands.
% Output the command line with `--verbose-commands'. This should be
% used for commands that may be of interest to the user.
% invoke_system_command(Globals, ProgressStream, CmdOutputStream,
% Verbosity, Command, Succeeded):
%
% Invoke an executable. Progress messages, including error messages
% that say why we cannot make progress, will go to ProgressStream.
% Output from the invoked command will go to CmdOutputStream.
%
:- pred invoke_system_command(globals::in, io.text_output_stream::in,
io.text_output_stream::in,
command_verbosity::in, string::in, maybe_succeeded::out,
io::di, io::uo) is det.
% invoke_system_command_maybe_filter_output(Globals,
% ProgressStream, CmdOutputStream, Verbosity, Command,
% MaybeProcessOutput, Succeeded):
%
% Invoke an executable. Progress messages, including error messages
% that say why we cannot make progress, will go to ProgressStream.
% Output from the invoked command, filtered if MaybeProcessOutput
% is set to yes(...), will go to CmdOutputStream.
%
:- pred invoke_system_command_maybe_filter_output(globals::in,
io.text_output_stream::in, io.text_output_stream::in,
command_verbosity::in, string::in, maybe(string)::in, maybe_succeeded::out,
io::di, io::uo) is det.
% invoke_long_system_command(Globals, ProgressStream, CmdOutputStream,
% Verbosity, Command, Args, Succeeded):
%
% Invoke an executable with arguments Args, using the @file style of
% calling to avoid command line length limits on various systems. If the
% underlying tool chain does not support this, it just calls the normal
% invoke_system_command and hopes the command is not too long. Progress
% messages, including error messages that say why we cannot make progress,
% will go to ProgressStream. Output from the invoked command will go to
% CmdOutputStream.
%
:- pred invoke_long_system_command(globals::in, io.text_output_stream::in,
io.text_output_stream::in, command_verbosity::in, string::in, string::in,
maybe_succeeded::out, io::di, io::uo) is det.
% invoke_long_system_command_maybe_filter_output(Globals,
% ProgressStream, CmdOutputStream, Verbosity, Command,
% NonAtArgs, Args, MaybeProcessOutput, Succeeded):
%
% Invoke an executable with arguments NonAtArgs and Args, using the @file
% style of calling for Args and passing NonAtArgs on the command line. If
% the underlying tool chain does not support this, it just calls the normal
% invoke_system_command and hopes the command is not too long. Progress
% messages, including error messages that say why we cannot make progress,
% will go to ProgressStream. Output from the invoked command, filtered if
% MaybeProcessOutput is set to yes(...), will go to CmdOutputStream.
%
:- pred invoke_long_system_command_maybe_filter_output(globals::in,
io.text_output_stream::in, io.text_output_stream::in,
command_verbosity::in, string::in, string::in, string::in,
maybe(string)::in, maybe_succeeded::out, io::di, io::uo) is det.
% Make a command string, which needs to be invoked in a shell environment.
%
:- pred make_command_string(string::in, quote_char::in, string::out) is det.
%-----------------------------------------------------------------------------%
%-----------------------------------------------------------------------------%
:- implementation.
:- import_module libs.file_util.
:- import_module libs.options.
:- import_module libs.process_util.
:- import_module bool.
:- import_module io.call_system.
:- import_module io.file.
:- import_module list.
:- import_module string.
%-----------------------------------------------------------------------------%
invoke_system_command(Globals, ProgressStream,
CmdOutputStream, Verbosity, Command, Succeeded, !IO) :-
invoke_system_command_maybe_filter_output(Globals, ProgressStream,
CmdOutputStream, Verbosity, Command, no, Succeeded, !IO).
invoke_system_command_maybe_filter_output(Globals, ProgressStream,
CmdOutputStream, Verbosity, Command, MaybeProcessOutput,
Succeeded, !IO) :-
% This predicate shouldn't alter the exit status of mercury_compile.
io.get_exit_status(OldStatus, !IO),
globals.lookup_bool_option(Globals, verbose, Verbose),
(
Verbosity = cmd_verbose,
PrintCommand = Verbose
;
Verbosity = cmd_verbose_commands,
globals.lookup_bool_option(Globals, verbose_commands, PrintCommand)
),
(
PrintCommand = yes,
io.format(ProgressStream,
"%% Invoking system command `%s'...\n", [s(Command)], !IO),
io.flush_output(ProgressStream, !IO)
;
PrintCommand = no
),
% The output from the command is written to a temporary file,
% which is then written to the output stream. Without this,
% the output from the command would go to the current C output
% and error streams.
io.file.make_temp_file(TmpFileResult, !IO),
(
TmpFileResult = ok(TmpFile),
( if use_dotnet then
% XXX can't use Bourne shell syntax to redirect on .NET
% XXX the output will go to the wrong place!
CommandRedirected = Command
else if use_win32 then
% On windows, we can't in general redirect standard error
% in the shell.
CommandRedirected = string.format("%s > %s",
[s(Command), s(TmpFile)])
else
CommandRedirected = string.format("%s > %s 2>&1",
[s(Command), s(TmpFile)])
),
io.call_system.call_system_return_signal(CommandRedirected,
CmdResult, !IO),
(
CmdResult = ok(exited(Status)),
maybe_write_string(ProgressStream, PrintCommand, "% done.\n", !IO),
( if Status = 0 then
CommandSucceeded = succeeded
else
% The command should have produced output describing the error.
CommandSucceeded = did_not_succeed
)
;
CmdResult = ok(signalled(Signal)),
% XXX The code handling signals is repeated below.
string.format("system command received signal %d.", [i(Signal)],
ErrorMsg),
report_arbitrary_error(ProgressStream, ErrorMsg, !IO),
% Also report the error to standard output, because if we raise the
% signal, this error may not ever been seen, the process stops, and
% the user is confused.
io.stdout_stream(StdOut, !IO),
report_arbitrary_error(StdOut, ErrorMsg, !IO),
% Make sure the current process gets the signal. Some systems (e.g.
% Linux) ignore SIGINT during a call to system().
raise_signal(Signal, !IO),
CommandSucceeded = did_not_succeed
;
CmdResult = error(CmdIOError),
CmdErrorMsg = io.error_message(CmdIOError),
report_arbitrary_error(ProgressStream, CmdErrorMsg, !IO),
CommandSucceeded = did_not_succeed
)
;
TmpFileResult = error(Error),
TmpMsg = "Could not create temporary file: " ++ error_message(Error),
report_arbitrary_error(ProgressStream, TmpMsg, !IO),
TmpFile = "",
CommandSucceeded = did_not_succeed
),
( if
MaybeProcessOutput = yes(ProcessOutput),
% We can't process the output in TmpFile if we could not *create*
% TmpFile.
TmpFileResult = ok(_),
% We can't do bash style redirection on .NET.
not use_dotnet
then
io.file.make_temp_file(ProcessedTmpFileResult, !IO),
(
ProcessedTmpFileResult = ok(ProcessedTmpFile),
MaybeProcessedTmpFile = ProcessedTmpFile,
% XXX we should get rid of use_win32
( if use_win32 then
get_system_env_type(Globals, SystemEnvType),
( if SystemEnvType = env_type_powershell then
ProcessOutputRedirected = string.format(
"Get-context %s | %s > %s 2>&1",
[s(TmpFile), s(ProcessOutput), s(ProcessedTmpFile)])
else
% On windows, we can't in general redirect standard
% error in the shell.
ProcessOutputRedirected = string.format("%s < %s > %s",
[s(ProcessOutput), s(TmpFile), s(ProcessedTmpFile)])
)
else
ProcessOutputRedirected = string.format("%s < %s > %s 2>&1",
[s(ProcessOutput), s(TmpFile), s(ProcessedTmpFile)])
),
(
PrintCommand = yes,
io.format(ProgressStream,
"%% Invoking system command `%s'...\n",
[s(ProcessOutputRedirected)], !IO),
io.flush_output(ProgressStream, !IO)
;
PrintCommand = no
),
io.call_system.call_system_return_signal(ProcessOutputRedirected,
ProcessOutputResult, !IO),
io.file.remove_file(TmpFile, _, !IO),
(
ProcessOutputResult = ok(exited(ProcessOutputStatus)),
maybe_write_string(ProgressStream, PrintCommand,
"% done.\n", !IO),
( if ProcessOutputStatus = 0 then
ProcessOutputSucceeded = succeeded
else
% The command should have produced output
% describing the error.
ProcessOutputSucceeded = did_not_succeed
)
;
ProcessOutputResult = ok(signalled(ProcessOutputSignal)),
% Make sure the current process gets the signal. Some systems
% (e.g. Linux) ignore SIGINT during a call to system().
raise_signal(ProcessOutputSignal, !IO),
string.format("system command received signal %d.",
[i(ProcessOutputSignal)], OutputSignalMsg),
report_arbitrary_error(ProgressStream, OutputSignalMsg, !IO),
ProcessOutputSucceeded = did_not_succeed
;
ProcessOutputResult = error(ProcessOutputError),
ProcessOutputErrorMsg = io.error_message(ProcessOutputError),
report_arbitrary_error(ProgressStream, ProcessOutputErrorMsg,
!IO),
ProcessOutputSucceeded = did_not_succeed
)
;
ProcessedTmpFileResult = error(ProcessTmpError),
ProcessTmpErrorMsg = io.error_message(ProcessTmpError),
report_arbitrary_error(ProgressStream, ProcessTmpErrorMsg, !IO),
ProcessOutputSucceeded = did_not_succeed,
MaybeProcessedTmpFile = TmpFile
)
else
ProcessOutputSucceeded = succeeded,
MaybeProcessedTmpFile = TmpFile
),
Succeeded = CommandSucceeded `and` ProcessOutputSucceeded,
% Write the output, if it exists, to the error stream.
% XXX Why do we try to do this EVEN WHEN the code above
% had not Succeeded?
% Answer: the user needs to see the output of Command most precisely when
% Command had *not* succeeded. However, if the failure occurs
% - when we try to create TmpFile
% - when we try to create ProcessedTmpFile,
% - during the execution of the filter command,
% then the user does not need to know the details of that failure.
%
% In the case of the first failure above, there is nothing useful
% we can print. In the case of the second and third failures,
% there *is* something useful we can print: the contents of TmpFile.
(
TmpFileResult = ok(_),
% MaybeProcessedTmpFile should be ProcessedTmpFile if our caller
% asked for filtering, and the filtering was successful. Otherwise,
% it should be TmpFile. In either case, if and when we get here,
% then MaybeProcessedTmpFile should definitely exist.
io.read_named_file_as_string(MaybeProcessedTmpFile,
MaybeProcessedTmpFileResult, !IO),
(
MaybeProcessedTmpFileResult = ok(MaybeProcessedTmpFileStr),
io.write_string(CmdOutputStream, MaybeProcessedTmpFileStr, !IO)
;
MaybeProcessedTmpFileResult = error(MaybeProcessedTmpFileError),
Msg = "error opening command output: " ++
io.error_message(MaybeProcessedTmpFileError),
report_arbitrary_error(ProgressStream, Msg, !IO)
),
io.file.remove_file(MaybeProcessedTmpFile, _, !IO)
;
TmpFileResult = error(_)
),
io.set_exit_status(OldStatus, !IO).
%-----------------------------------------------------------------------------%
invoke_long_system_command(Globals,
ProgressStream, CmdOutputStream, Verbosity,
Cmd, Args, Succeeded, !IO) :-
invoke_long_system_command_maybe_filter_output(Globals,
ProgressStream, CmdOutputStream, Verbosity,
Cmd, "", Args, no, Succeeded, !IO).
invoke_long_system_command_maybe_filter_output(Globals,
ProgressStream, CmdOutputStream, Verbosity,
Cmd, NonAtArgs, Args, MaybeProcessOutput, Succeeded, !IO) :-
globals.lookup_bool_option(Globals, restricted_command_line,
RestrictedCommandLine),
(
RestrictedCommandLine = yes,
% Avoid generating very long command lines by using @files.
open_temp_output(TmpFileResult, !IO),
(
TmpFileResult = ok({TmpFile, TmpStream}),
% We need to escape any \ before writing them to the file,
% otherwise we lose them.
TmpFileArgs = string.replace_all(Args, "\\", "\\\\"),
io.write_string(TmpStream, TmpFileArgs, !IO),
io.close_output(TmpStream, !IO),
globals.lookup_bool_option(Globals, very_verbose, VeryVerbose),
AtFileName = at_file_name(Globals, TmpFile),
(
VeryVerbose = yes,
io.format(ProgressStream, "%% Args placed in %s: `%s'\n",
[s(AtFileName), s(TmpFileArgs)], !IO),
io.flush_output(ProgressStream, !IO)
;
VeryVerbose = no
),
( if NonAtArgs = "" then
string.format("%s %s", [s(Cmd), s(AtFileName)], FullCmd)
else
string.format("%s %s %s",
[s(Cmd), s(NonAtArgs), s(AtFileName)], FullCmd)
),
invoke_system_command_maybe_filter_output(Globals,
ProgressStream, CmdOutputStream, Verbosity,
FullCmd, MaybeProcessOutput, Succeeded0, !IO),
io.file.remove_file(TmpFile, RemoveResult, !IO),
(
RemoveResult = ok,
Succeeded = Succeeded0
;
RemoveResult = error(_),
Succeeded = did_not_succeed
)
;
TmpFileResult = error(ErrorMessage),
io.format(ProgressStream, "%s\n", [s(ErrorMessage)], !IO),
Succeeded = did_not_succeed
)
;
RestrictedCommandLine = no,
( if NonAtArgs = "" then
string.format("%s %s", [s(Cmd), s(Args)], FullCmd)
else
string.format("%s %s %s", [s(Cmd), s(NonAtArgs), s(Args)], FullCmd)
),
invoke_system_command_maybe_filter_output(Globals,
ProgressStream, CmdOutputStream, Verbosity,
FullCmd, MaybeProcessOutput, Succeeded, !IO)
).
% Form the name of an @file given a file name.
% On some systems we need to escape the `@' character.
%
:- func at_file_name(globals, string) = string.
at_file_name(Globals, FileName) = AtFileName :-
get_system_env_type(Globals, EnvType),
(
EnvType = env_type_powershell,
AtFileName = "`@" ++ FileName
;
( EnvType = env_type_posix
; EnvType = env_type_cygwin
; EnvType = env_type_msys
; EnvType = env_type_win_cmd
),
AtFileName = "@" ++ FileName
).
%-----------------------------------------------------------------------------%
make_command_string(String0, QuoteType, String) :-
( if use_win32 then
(
QuoteType = forward,
Quote = " '"
;
QuoteType = double,
Quote = " """
),
string.append_list(["sh -c ", Quote, String0, Quote], String)
else
String = String0
).
%-----------------------------------------------------------------------------%
% Are we compiling in a .NET environment?
%
:- pred use_dotnet is semidet.
:- pragma foreign_proc("C#",
use_dotnet,
[will_not_call_mercury, promise_pure, thread_safe],
"
SUCCESS_INDICATOR = true;
").
% The following clause is only used if there is no matching foreign_proc.
use_dotnet :-
semidet_fail.
% Are we compiling in a win32 environment?
%
% If in doubt, use_win32 should succeed. This is only used to decide
% whether to invoke Bourne shell command and shell scripts directly,
% or whether to invoke them via `sh -c ...'. The latter should work
% correctly in a Unix environment too, but is a little less efficient
% since it invokes another process.
%
:- pred use_win32 is semidet.
:- pragma foreign_proc("C",
use_win32,
[will_not_call_mercury, promise_pure, thread_safe],
"
#ifdef MR_WIN32
SUCCESS_INDICATOR = 1;
#else
SUCCESS_INDICATOR = 0;
#endif
").
% The following clause is only used if there is no matching foreign_proc.
% See comment above for why it is OK to just succeed here.
use_win32 :-
semidet_succeed.
%-----------------------------------------------------------------------------%
:- end_module system_cmds.
%-----------------------------------------------------------------------------%