%---------------------------------------------------------------------------% % vim: ft=mercury ts=4 sw=4 et %---------------------------------------------------------------------------% % Copyright (C) 2002-2012 The University of Melbourne. % Copyright (C) 2013-2021, 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: make.track_flags.m. % % This module keeps track of which options were used to compile which modules. % %---------------------------------------------------------------------------% :- module make.track_flags. :- interface. :- import_module libs. :- import_module libs.globals. :- import_module libs.maybe_util. :- import_module make.make_info. :- import_module mdbcomp. :- import_module mdbcomp.sym_name. :- import_module io. % Generate the .track_flags files for local modules reachable from the % target module. The files contain hashes of the options which are set for % that particular module (deliberately ignoring some options), and are only % updated if they have changed since the last --make run. We use hashes % as the full option tables are quite large. % :- pred make_track_flags_files(io.text_output_stream::in, io.text_output_stream::in, globals::in, module_name::in, maybe_succeeded::out, make_info::in, make_info::out, io::di, io::uo) is det. %---------------------------------------------------------------------------% %---------------------------------------------------------------------------% :- implementation. :- import_module libs.handle_options. :- import_module libs.md5. :- import_module libs.options. :- import_module make.find_local_modules. :- import_module make.options_file. :- import_module parse_tree. :- import_module parse_tree.error_spec. :- import_module parse_tree.file_names. :- import_module parse_tree.maybe_error. :- import_module parse_tree.write_error_spec. :- import_module bool. :- import_module getopt. :- import_module list. :- import_module map. :- import_module maybe. :- import_module pair. :- import_module require. :- import_module set. :- import_module set_tree234. :- import_module string. %---------------------------------------------------------------------------% :- type last_hash ---> last_hash( lh_maybe_stdlib_grades :: maybe_stdlib_grades, lh_maybe_stdlib_dirs :: maybe1(list(string)), lh_options :: list(string), lh_hash :: string ). make_track_flags_files(ErrorStream, ProgressStream, Globals, ModuleName, Succeeded, !Info, !IO) :- find_reachable_local_modules(ProgressStream, Globals, ModuleName, Succeeded0, ModuleNames, !Info, !IO), ( Succeeded0 = succeeded, DummyLastHash = last_hash(stdlib_grades_unknown, error1([]), [], ""), foldl3_make_track_flags_for_modules_loop(ErrorStream, ProgressStream, Globals, set.to_sorted_list(ModuleNames), Succeeded, DummyLastHash, _LastHash, !Info, !IO) ; Succeeded0 = did_not_succeed, Succeeded = did_not_succeed ). %---------------------------------------------------------------------------% :- pred foldl3_make_track_flags_for_modules_loop(io.text_output_stream::in, io.text_output_stream::in, globals::in, list(module_name)::in, maybe_succeeded::out, last_hash::in, last_hash::out, make_info::in, make_info::out, io::di, io::uo) is det. foldl3_make_track_flags_for_modules_loop(_ErrorStream, _ProgressStream, _Globals, [], succeeded, !LastHash, !Info, !IO). foldl3_make_track_flags_for_modules_loop(ErrorStream, ProgressStream, Globals, [ModuleName | ModuleNames], Succeeded, !LastHash, !Info, !IO) :- make_track_flags_files_for_module(ErrorStream, ProgressStream, Globals, !.Info, ModuleName, Succeeded0, !LastHash, !IO), ( Succeeded0 = succeeded, foldl3_make_track_flags_for_modules_loop(ErrorStream, ProgressStream, Globals, ModuleNames, Succeeded, !LastHash, !Info, !IO) ; Succeeded0 = did_not_succeed, Succeeded = did_not_succeed ). %---------------------------------------------------------------------------% :- pred make_track_flags_files_for_module(io.text_output_stream::in, io.text_output_stream::in, globals::in, make_info::in, module_name::in, maybe_succeeded::out, last_hash::in, last_hash::out, io::di, io::uo) is det. make_track_flags_files_for_module(ErrorStream, ProgressStream, Globals, Info, ModuleName, Succeeded, !LastHash, !IO) :- Params = make_info_get_compiler_params(Info), Params = compiler_params(EnvOptFileVariables, EnvVarArgs, OptionArgs), lookup_mmc_module_options(EnvOptFileVariables, ModuleName, MaybeModuleOptionArgs), ( MaybeModuleOptionArgs = ok1(ModuleOptionArgs), MaybeStdLibGrades = make_info_get_maybe_stdlib_grades(Info), lookup_mercury_stdlib_dir(EnvOptFileVariables, MaybeStdLibDirs), AllOptionArgs = ModuleOptionArgs ++ EnvVarArgs ++ OptionArgs, % The set of options from one module to the next is usually identical, % so we can easily avoid running handle_options and stringifying and % hashing the option table, all of which can contribute to an annoying % delay when mmc --make starts. ( if !.LastHash = last_hash(MaybeStdLibGrades, MaybeStdLibDirs, AllOptionArgs, HashPrime) then Hash = HashPrime else get_default_options(Globals, DefaultOptionTable), option_table_hash(ProgressStream, DefaultOptionTable, MaybeStdLibGrades, MaybeStdLibDirs, AllOptionArgs, Hash, !IO), !:LastHash = last_hash(MaybeStdLibGrades, MaybeStdLibDirs, AllOptionArgs, Hash) ), % XXX LEGACY module_name_to_file_name_create_dirs(Globals, $pred, ext_cur_ngs_gs(ext_cur_ngs_gs_misc_track_flags), ModuleName, HashFileName, _HashFileNameProposed, !IO), compare_hash_file(ProgressStream, Globals, HashFileName, Hash, Same, !IO), ( Same = yes, Succeeded = succeeded ; Same = no, write_hash_file(ProgressStream, HashFileName, Hash, Succeeded, !IO) ) ; MaybeModuleOptionArgs = error1(LookupSpecs), % XXX ErrorStream is the error stream for the top target, % which may or may NOT be ModuleName. It is not clear to me (zs) % whether the right place to write LookupSpecs is ErrorStream, % or a stream wrting to the .err file of ModuleName. % I also don't think we have a good basis for deciding the answer % to that question until we have practical experience with % compiler invocations that lead execution to this spot. write_error_specs(ErrorStream, Globals, LookupSpecs, !IO), Succeeded = did_not_succeed ). %---------------------------------------------------------------------------% :- pred option_table_hash(io.text_output_stream::in, option_table::in, maybe_stdlib_grades::in, maybe1(list(string))::in, list(string)::in, string::out, io::di, io::uo) is det. option_table_hash(ProgressStream, DefaultOptionTable, MaybeStdLibGrades, MaybeStdLibDirs, AllOptionArgs, Hash, !IO) :- % This code is part of the --track-flags implementation. We hash the % options in the updated globals because they include module-specific % options. The hash is then compared with the hash stored in the % .track_flags file, which is updated if it differs. % The new timestamp will later force the module to be recompiled % if necessary, but that's later. We are not compiling the module % immediately, so this is the only use we have for AllOptionArgsGlobals % here. % % XXX This algorithm processes every option in the option table, even % though it does not include all of them in the hash. Virtually all % of these options will have their default values, which makes % most of that work effectively wasted. % % A more elegant approach would be to invoke getopt.recognize_all_options % on AllOptionArgs, and hash the resulting list of option_values. % This would require hashing special options (such as -ON) as well as % non-special values, but the cost of that would be trivial. % % This approach would have two principal differences from the current one. % % - First, the current approach computes a different hash if AllOptionArgs % has not changed, but the default value of an (consequential) option % has changed, or if a new consequential option has been added. % The recompilation that this forces will be needed after some changes % to the option defaults, but not after others. % % However, this consideration never applies only to a single module; % if the default set of option values changes, it applies for all % modules. Therefore it would be enough to record a hash of the % default values of all options (or a timestamp when the option database % was last modified, see below) *once* in a global file that is relevant % to all modules in a directory, named maybe "Mercury.track_flags", % which, if it changes, invalidates *every* .track_flags % file in that directory. % % - Second, it is possible for some changes in AllOptionArgs to yield % the same final AllOptionArgsGlobals, if some option in the old % AllOptionArgs implies the value of some other option, and the new % AllOptionArgs explicitly sets this option to the implied value % (or vice versa). In such cases, the new approach would force a % recompilation, while the old one would not. However, the absence % of a recompilation in such an instance could be more worrysome % than welcome for users who do not know about that option implication, % or who do not appreciate its significance. % % Note also that while the old approach forces a recompilation % both when the set of options changes and when the default value % of an option changes, it does *not* force a recompilation % if the *meaning* of an option is redefined. We could consider % all three of these kinds of changes grounds for updating a timestamp % of when the option database last changed. % % XXX A somewhat more backwards-compatible version of the above approach % would be to hash both DefaultOptionTable AND the result invoking % getopt.recognize_all_options, after filtering inconsequential options % out of both. This would effectively hash the *input* of the call to % handle_given_options, rather than its output. It would also be % logically cleaner, since in some cases, handle_given_options does % updates on mutables (e.g. for disabling smart recompilation) % whose effects persist *beyond* the processing of a given module. % handle_given_options(ProgressStream, DefaultOptionTable, MaybeStdLibGrades, MaybeStdLibDirs, AllOptionArgs, _, _, OptionsErrors, AllOptionArgsGlobals, !IO), ( OptionsErrors = [] ; OptionsErrors = [_ | _], unexpected($file, $pred ++ ": " ++ "handle_options returned with errors") ), globals.get_options(AllOptionArgsGlobals, OptionTable), map.to_sorted_assoc_list(OptionTable, OptionList), DoNotTrackOptions = set_tree234.list_to_set(options_not_to_track), list.filter(include_option_in_hash(DoNotTrackOptions), OptionList, HashOptionList), globals.get_opt_tuple(AllOptionArgsGlobals, OptTuple), Hash = md5sum(string({HashOptionList, OptTuple})). :- pred include_option_in_hash(set_tree234(option)::in, pair(option, option_data)::in) is semidet. include_option_in_hash(InconsequentialOptionsSet, Option - OptionData) :- require_complete_switch [OptionData] ( ( OptionData = bool(_) ; OptionData = int(_) ; OptionData = string(_) ; OptionData = maybe_int(_) ; OptionData = maybe_string(_) ; OptionData = accumulating(_) ), % XXX Reconsider if a lot of these options really should be ignored. not set_tree234.contains(InconsequentialOptionsSet, Option) ; ( OptionData = special ; OptionData = bool_special ; OptionData = int_special ; OptionData = string_special ; OptionData = maybe_string_special ; OptionData = file_special ), % There is no point hashing special options as the option_data is % always the same. fail ). :- pred compare_hash_file(io.text_output_stream::in, globals::in, string::in, string::in, bool::out, io::di, io::uo) is det. compare_hash_file(ProgressStream, Globals, FileName, Hash, Same, !IO) :- io.open_input(FileName, OpenResult, !IO), ( OpenResult = ok(Stream), io.read_line_as_string(Stream, ReadResult, !IO), ( ReadResult = ok(Line), ( if Line = Hash then Same = yes else Same = no ) ; ReadResult = eof, Same = no ; ReadResult = error(_), Same = no ), io.close_input(Stream, !IO) ; OpenResult = error(_), % Probably missing file. Same = no ), globals.lookup_bool_option(Globals, verbose, Verbose), ( Verbose = yes, ( Same = yes, io.format(ProgressStream, "%% %s does not need updating.\n", [s(FileName)], !IO) ; Same = no, io.format(ProgressStream, "%% %s will be UPDATED.\n", [s(FileName)], !IO) ) ; Verbose = no ). :- pred write_hash_file(io.text_output_stream::in, string::in, string::in, maybe_succeeded::out, io::di, io::uo) is det. write_hash_file(ProgressStream, FileName, Hash, Succeeded, !IO) :- io.open_output(FileName, OpenResult, !IO), ( OpenResult = ok(Stream), io.write_string(Stream, Hash, !IO), io.close_output(Stream, !IO), Succeeded = succeeded ; OpenResult = error(Error), io.format(ProgressStream, "Error creating `%s': %s\n", [s(FileName), s(io.error_message(Error))], !IO), Succeeded = did_not_succeed ). %---------------------------------------------------------------------------% :- end_module make.track_flags. %---------------------------------------------------------------------------%