View source with formatted comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        J.Wielemaker@vu.nl
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2012-2021, VU University Amsterdam
    7                              CWI, Amsterdam
    8                              SWI-Prolog Solutions b.v.
    9    All rights reserved.
   10
   11    Redistribution and use in source and binary forms, with or without
   12    modification, are permitted provided that the following conditions
   13    are met:
   14
   15    1. Redistributions of source code must retain the above copyright
   16       notice, this list of conditions and the following disclaimer.
   17
   18    2. Redistributions in binary form must reproduce the above copyright
   19       notice, this list of conditions and the following disclaimer in
   20       the documentation and/or other materials provided with the
   21       distribution.
   22
   23    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   24    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   25    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   26    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   27    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   28    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   29    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   30    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   31    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   32    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   33    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   34    POSSIBILITY OF SUCH DAMAGE.
   35*/
   36
   37:- module(prolog_pack,
   38          [ pack_list_installed/0,
   39            pack_info/1,                % +Name
   40            pack_list/1,                % +Keyword
   41            pack_search/1,              % +Keyword
   42            pack_install/1,             % +Name
   43            pack_install/2,             % +Name, +Options
   44            pack_upgrade/1,             % +Name
   45            pack_rebuild/1,             % +Name
   46            pack_rebuild/0,             % All packages
   47            pack_remove/1,              % +Name
   48            pack_property/2,            % ?Name, ?Property
   49            pack_attach/2,              % +Dir, +Options
   50
   51            pack_url_file/2             % +URL, -File
   52          ]).   53:- use_module(library(apply)).   54:- use_module(library(error)).   55:- use_module(library(option)).   56:- use_module(library(readutil)).   57:- use_module(library(lists)).   58:- use_module(library(filesex)).   59:- use_module(library(xpath)).   60:- use_module(library(settings)).   61:- use_module(library(uri)).   62:- use_module(library(dcg/basics)).   63:- use_module(library(http/http_open)).   64:- use_module(library(http/json)).   65:- use_module(library(http/http_client), []).   % plugin for POST support
   66:- use_module(library(prolog_config)).   67:- use_module(library(debug), [assertion/1]).   68:- use_module(library(pairs), [group_pairs_by_key/2]).   69% Stuff we may not have and may not need
   70:- autoload(library(git)).   71:- autoload(library(sgml)).   72:- autoload(library(sha)).   73:- autoload(library(build/tools)).   74
   75/** <module> A package manager for Prolog
   76
   77The library(prolog_pack) provides the SWI-Prolog   package manager. This
   78library lets you inspect installed   packages,  install packages, remove
   79packages, etc. It is complemented by   the  built-in attach_packs/0 that
   80makes installed packages available as libraries.
   81
   82To make changes to a package:
   83  * Clone the git repo,t go into the repo and run:
   84    `?- pack_install(.).`
   85    This builds the pack locally and creates a symlink to make it available.
   86  * `?- pack_rebuild(package_name).`
   87    This runs `make distclean` and `make` with the right environment. It will also
   88    write a file `buildendv.sh` that you can source to get the environment
   89    for running a normal `make` (this is done only if there is a `configure`
   90    step; i.e., if there is a `configure.in` or `configure`.
   91  * The build process also supports `cmake`.
   92
   93Once you have made the changes, you should edit the `pack.pl` file
   94to change the `version` item. After updating the git repo, issue
   95a `pack_install(package_name, [upgrade(true), test(true), rebuild(make)])`
   96to cause the repository to refresh. You can simulate the full
   97installation process by removing all the build files in the package
   98(including any in submodules), running pack_install/1, and then
   99running pack_install using a =|file://|= URL.
  100
  101@see    Installed packages can be inspected using =|?- doc_browser.|=
  102@see    library(build/tools)
  103@tbd    Version logic
  104@tbd    Find and resolve conflicts
  105@tbd    Upgrade git packages
  106@tbd    Validate git packages
  107@tbd    Test packages: run tests from directory `test'.
  108*/
  109
  110:- multifile
  111    environment/2.                          % Name, Value
  112
  113:- dynamic
  114    pack_requires/2,                        % Pack, Requirement
  115    pack_provides_db/2.                     % Pack, Provided
  116
  117
  118                 /*******************************
  119                 *          CONSTANTS           *
  120                 *******************************/
  121
  122:- setting(server, atom, 'https://www.swi-prolog.org/pack/',
  123           'Server to exchange pack information').  124
  125
  126                 /*******************************
  127                 *         PACKAGE INFO         *
  128                 *******************************/
  129
  130%!  current_pack(?Pack) is nondet.
  131%!  current_pack(?Pack, ?Dir) is nondet.
  132%
  133%   True if Pack is a currently installed pack.
  134
  135current_pack(Pack) :-
  136    current_pack(Pack, _).
  137
  138current_pack(Pack, Dir) :-
  139    '$pack':pack(Pack, Dir).
  140
  141%!  pack_list_installed is det.
  142%
  143%   List currently installed  packages.   Unlike  pack_list/1,  only
  144%   locally installed packages are displayed   and  no connection is
  145%   made to the internet.
  146%
  147%   @see Use pack_list/1 to find packages.
  148
  149pack_list_installed :-
  150    findall(Pack, current_pack(Pack), Packages0),
  151    Packages0 \== [],
  152    !,
  153    sort(Packages0, Packages),
  154    length(Packages, Count),
  155    format('Installed packages (~D):~n~n', [Count]),
  156    maplist(pack_info(list), Packages),
  157    validate_dependencies.
  158pack_list_installed :-
  159    print_message(informational, pack(no_packages_installed)).
  160
  161%!  pack_info(+Pack)
  162%
  163%   Print more detailed information about Pack.
  164
  165pack_info(Name) :-
  166    pack_info(info, Name).
  167
  168pack_info(Level, Name) :-
  169    must_be(atom, Name),
  170    findall(Info, pack_info(Name, Level, Info), Infos0),
  171    (   Infos0 == []
  172    ->  print_message(warning, pack(no_pack_installed(Name))),
  173        fail
  174    ;   true
  175    ),
  176    update_dependency_db(Name, Infos0),
  177    findall(Def,  pack_default(Level, Infos, Def), Defs),
  178    append(Infos0, Defs, Infos1),
  179    sort(Infos1, Infos),
  180    show_info(Name, Infos, [info(Level)]).
  181
  182
  183show_info(_Name, _Properties, Options) :-
  184    option(silent(true), Options),
  185    !.
  186show_info(Name, Properties, Options) :-
  187    option(info(list), Options),
  188    !,
  189    memberchk(title(Title), Properties),
  190    memberchk(version(Version), Properties),
  191    format('i ~w@~w ~28|- ~w~n', [Name, Version, Title]).
  192show_info(Name, Properties, _) :-
  193    !,
  194    print_property_value('Package'-'~w', [Name]),
  195    findall(Term, pack_level_info(info, Term, _, _), Terms),
  196    maplist(print_property(Properties), Terms).
  197
  198print_property(_, nl) :-
  199    !,
  200    format('~n').
  201print_property(Properties, Term) :-
  202    findall(Term, member(Term, Properties), Terms),
  203    Terms \== [],
  204    !,
  205    pack_level_info(_, Term, LabelFmt, _Def),
  206    (   LabelFmt = Label-FmtElem
  207    ->  true
  208    ;   Label = LabelFmt,
  209        FmtElem = '~w'
  210    ),
  211    multi_valued(Terms, FmtElem, FmtList, Values),
  212    atomic_list_concat(FmtList, ', ', Fmt),
  213    print_property_value(Label-Fmt, Values).
  214print_property(_, _).
  215
  216multi_valued([H], LabelFmt, [LabelFmt], Values) :-
  217    !,
  218    H =.. [_|Values].
  219multi_valued([H|T], LabelFmt, [LabelFmt|LT], Values) :-
  220    H =.. [_|VH],
  221    append(VH, MoreValues, Values),
  222    multi_valued(T, LabelFmt, LT, MoreValues).
  223
  224
  225pvalue_column(24).
  226print_property_value(Prop-Fmt, Values) :-
  227    !,
  228    pvalue_column(C),
  229    atomic_list_concat(['~w:~t~*|', Fmt, '~n'], Format),
  230    format(Format, [Prop,C|Values]).
  231
  232pack_info(Name, Level, Info) :-
  233    '$pack':pack(Name, BaseDir),
  234    (   Info = directory(BaseDir)
  235    ;   pack_info_term(BaseDir, Info)
  236    ),
  237    pack_level_info(Level, Info, _Format, _Default).
  238
  239:- public pack_level_info/4.                    % used by web-server
  240
  241pack_level_info(_,    title(_),         'Title',                   '<no title>').
  242pack_level_info(_,    version(_),       'Installed version',       '<unknown>').
  243pack_level_info(info, directory(_),     'Installed in directory',  -).
  244pack_level_info(info, author(_, _),     'Author'-'~w <~w>',        -).
  245pack_level_info(info, maintainer(_, _), 'Maintainer'-'~w <~w>',    -).
  246pack_level_info(info, packager(_, _),   'Packager'-'~w <~w>',      -).
  247pack_level_info(info, home(_),          'Home page',               -).
  248pack_level_info(info, download(_),      'Download URL',            -).
  249pack_level_info(_,    provides(_),      'Provides',                -).
  250pack_level_info(_,    requires(_),      'Requires',                -).
  251pack_level_info(_,    conflicts(_),     'Conflicts with',          -).
  252pack_level_info(_,    replaces(_),      'Replaces packages',       -).
  253pack_level_info(info, library(_),	'Provided libraries',      -).
  254
  255pack_default(Level, Infos, Def) :-
  256    pack_level_info(Level, ITerm, _Format, Def),
  257    Def \== (-),
  258    \+ memberchk(ITerm, Infos).
  259
  260%!  pack_info_term(+PackDir, ?Info) is nondet.
  261%
  262%   True when Info is meta-data for the package PackName.
  263
  264pack_info_term(BaseDir, Info) :-
  265    directory_file_path(BaseDir, 'pack.pl', InfoFile),
  266    catch(
  267        setup_call_cleanup(
  268            open(InfoFile, read, In),
  269            term_in_stream(In, Info),
  270            close(In)),
  271        error(existence_error(source_sink, InfoFile), _),
  272        ( print_message(error, pack(no_meta_data(BaseDir))),
  273          fail
  274        )).
  275pack_info_term(BaseDir, library(Lib)) :-
  276    atom_concat(BaseDir, '/prolog/', LibDir),
  277    atom_concat(LibDir, '*.pl', Pattern),
  278    expand_file_name(Pattern, Files),
  279    maplist(atom_concat(LibDir), Plain, Files),
  280    convlist(base_name, Plain, Libs),
  281    member(Lib, Libs).
  282
  283base_name(File, Base) :-
  284    file_name_extension(Base, pl, File).
  285
  286term_in_stream(In, Term) :-
  287    repeat,
  288        read_term(In, Term0, []),
  289        (   Term0 == end_of_file
  290        ->  !, fail
  291        ;   Term = Term0,
  292            valid_info_term(Term0)
  293        ).
  294
  295valid_info_term(Term) :-
  296    Term =.. [Name|Args],
  297    same_length(Args, Types),
  298    Decl =.. [Name|Types],
  299    (   pack_info_term(Decl)
  300    ->  maplist(valid_info_arg, Types, Args)
  301    ;   print_message(warning, pack(invalid_info(Term))),
  302        fail
  303    ).
  304
  305valid_info_arg(Type, Arg) :-
  306    must_be(Type, Arg).
  307
  308%!  pack_info_term(?Term) is nondet.
  309%
  310%   True when Term describes name and   arguments of a valid package
  311%   info term.
  312
  313pack_info_term(name(atom)).                     % Synopsis
  314pack_info_term(title(atom)).
  315pack_info_term(keywords(list(atom))).
  316pack_info_term(description(list(atom))).
  317pack_info_term(version(version)).
  318pack_info_term(author(atom, email_or_url_or_empty)).     % Persons
  319pack_info_term(maintainer(atom, email_or_url)).
  320pack_info_term(packager(atom, email_or_url)).
  321pack_info_term(pack_version(nonneg)).           % Package convention version
  322pack_info_term(home(atom)).                     % Home page
  323pack_info_term(download(atom)).                 % Source
  324pack_info_term(provides(atom)).                 % Dependencies
  325pack_info_term(requires(dependency)).
  326pack_info_term(conflicts(dependency)).          % Conflicts with package
  327pack_info_term(replaces(atom)).                 % Replaces another package
  328pack_info_term(autoload(boolean)).              % Default installation options
  329
  330:- multifile
  331    error:has_type/2.  332
  333error:has_type(version, Version) :-
  334    atom(Version),
  335    version_data(Version, _Data).
  336error:has_type(email_or_url, Address) :-
  337    atom(Address),
  338    (   sub_atom(Address, _, _, _, @)
  339    ->  true
  340    ;   uri_is_global(Address)
  341    ).
  342error:has_type(email_or_url_or_empty, Address) :-
  343    (   Address == ''
  344    ->  true
  345    ;   error:has_type(email_or_url, Address)
  346    ).
  347error:has_type(dependency, Value) :-
  348    is_dependency(Value, _Token, _Version).
  349
  350version_data(Version, version(Data)) :-
  351    atomic_list_concat(Parts, '.', Version),
  352    maplist(atom_number, Parts, Data).
  353
  354is_dependency(Token, Token, *) :-
  355    atom(Token).
  356is_dependency(Term, Token, VersionCmp) :-
  357    Term =.. [Op,Token,Version],
  358    cmp(Op, _),
  359    version_data(Version, _),
  360    VersionCmp =.. [Op,Version].
  361
  362cmp(<,  @<).
  363cmp(=<, @=<).
  364cmp(==, ==).
  365cmp(>=, @>=).
  366cmp(>,  @>).
  367
  368
  369                 /*******************************
  370                 *            SEARCH            *
  371                 *******************************/
  372
  373%!  pack_search(+Query) is det.
  374%!  pack_list(+Query) is det.
  375%
  376%   Query package server and installed packages and display results.
  377%   Query is matches case-insensitively against   the name and title
  378%   of known and installed packages. For   each  matching package, a
  379%   single line is displayed that provides:
  380%
  381%     - Installation status
  382%       - *p*: package, not installed
  383%       - *i*: installed package; up-to-date with public version
  384%       - *U*: installed package; can be upgraded
  385%       - *A*: installed package; newer than publically available
  386%       - *l*: installed package; not on server
  387%     - Name@Version
  388%     - Name@Version(ServerVersion)
  389%     - Title
  390%
  391%   Hint: =|?- pack_list('').|= lists all packages.
  392%
  393%   The predicates pack_list/1 and pack_search/1  are synonyms. Both
  394%   contact the package server at  http://www.swi-prolog.org to find
  395%   available packages.
  396%
  397%   @see    pack_list_installed/0 to list installed packages without
  398%           contacting the server.
  399
  400pack_list(Query) :-
  401    pack_search(Query).
  402
  403pack_search(Query) :-
  404    query_pack_server(search(Query), Result, []),
  405    (   Result == false
  406    ->  (   local_search(Query, Packs),
  407            Packs \== []
  408        ->  forall(member(pack(Pack, Stat, Title, Version, _), Packs),
  409                   format('~w ~w@~w ~28|- ~w~n',
  410                          [Stat, Pack, Version, Title]))
  411        ;   print_message(warning, pack(search_no_matches(Query)))
  412        )
  413    ;   Result = true(Hits),
  414        local_search(Query, Local),
  415        append(Hits, Local, All),
  416        sort(All, Sorted),
  417        list_hits(Sorted)
  418    ).
  419
  420list_hits([]).
  421list_hits([ pack(Pack, i, Title, Version, _),
  422            pack(Pack, p, Title, Version, _)
  423          | More
  424          ]) :-
  425    !,
  426    format('i ~w@~w ~28|- ~w~n', [Pack, Version, Title]),
  427    list_hits(More).
  428list_hits([ pack(Pack, i, Title, VersionI, _),
  429            pack(Pack, p, _,     VersionS, _)
  430          | More
  431          ]) :-
  432    !,
  433    version_data(VersionI, VDI),
  434    version_data(VersionS, VDS),
  435    (   VDI @< VDS
  436    ->  Tag = ('U')
  437    ;   Tag = ('A')
  438    ),
  439    format('~w ~w@~w(~w) ~28|- ~w~n', [Tag, Pack, VersionI, VersionS, Title]),
  440    list_hits(More).
  441list_hits([ pack(Pack, i, Title, VersionI, _)
  442          | More
  443          ]) :-
  444    !,
  445    format('l ~w@~w ~28|- ~w~n', [Pack, VersionI, Title]),
  446    list_hits(More).
  447list_hits([pack(Pack, Stat, Title, Version, _)|More]) :-
  448    format('~w ~w@~w ~28|- ~w~n', [Stat, Pack, Version, Title]),
  449    list_hits(More).
  450
  451
  452local_search(Query, Packs) :-
  453    findall(Pack, matching_installed_pack(Query, Pack), Packs).
  454
  455matching_installed_pack(Query, pack(Pack, i, Title, Version, URL)) :-
  456    current_pack(Pack),
  457    findall(Term,
  458            ( pack_info(Pack, _, Term),
  459              search_info(Term)
  460            ), Info),
  461    (   sub_atom_icasechk(Pack, _, Query)
  462    ->  true
  463    ;   memberchk(title(Title), Info),
  464        sub_atom_icasechk(Title, _, Query)
  465    ),
  466    option(title(Title), Info, '<no title>'),
  467    option(version(Version), Info, '<no version>'),
  468    option(download(URL), Info, '<no download url>').
  469
  470search_info(title(_)).
  471search_info(version(_)).
  472search_info(download(_)).
  473
  474
  475                 /*******************************
  476                 *            INSTALL           *
  477                 *******************************/
  478
  479%!  pack_install(+Spec:atom) is det.
  480%
  481%   Install a package.  Spec is one of
  482%
  483%     * A package name.  This queries the package repository
  484%       at http://www.swi-prolog.org
  485%     * Archive file name
  486%     * HTTP URL of an archive file name.  This URL may contain a
  487%       star (*) for the version.  In this case pack_install asks
  488%       for the directory content and selects the latest version.
  489%     * GIT URL (not well supported yet)
  490%     * A local directory name given as =|file://|= URL
  491%     * `'.'`, in which case a relative symlink is created to the
  492%       current directory (all other options for Spec make a copy
  493%       of the files).
  494%
  495%   After resolving the type of package,   pack_install/2 is used to
  496%   do the actual installation.
  497
  498pack_install(Spec) :-
  499    pack_default_options(Spec, Pack, [], Options),
  500    pack_install(Pack, [pack(Pack)|Options]).
  501
  502%!  pack_default_options(+Spec, -Pack, +OptionsIn, -Options) is det.
  503%
  504%   Establish  the  pack  name  (Pack)  and    install  options  from  a
  505%   specification and options (OptionsIn) provided by the user.
  506
  507pack_default_options(_Spec, Pack, OptsIn, Options) :-
  508    option(already_installed(pack(Pack,_Version)), OptsIn),
  509    !,
  510    Options = OptsIn.
  511pack_default_options(_Spec, Pack, OptsIn, Options) :-
  512    option(url(URL), OptsIn),
  513    !,
  514    (   option(git(_), OptsIn)
  515    ->  Options = OptsIn
  516    ;   git_url(URL, Pack)
  517    ->  Options = [git(true)|OptsIn]
  518    ;   Options = OptsIn
  519    ),
  520    (   nonvar(Pack)
  521    ->  true
  522    ;   option(pack(Pack), Options)
  523    ->  true
  524    ;   pack_version_file(Pack, _Version, URL)
  525    ).
  526pack_default_options(Archive, Pack, _, Options) :-      % Install from archive
  527    must_be(atom, Archive),
  528    \+ uri_is_global(Archive),
  529    expand_file_name(Archive, [File]),
  530    exists_file(File),
  531    !,
  532    pack_version_file(Pack, Version, File),
  533    uri_file_name(FileURL, File),
  534    Options = [url(FileURL), version(Version)].
  535pack_default_options(URL, Pack, _, Options) :-
  536    git_url(URL, Pack),
  537    !,
  538    Options = [git(true), url(URL)].
  539pack_default_options(FileURL, Pack, _, Options) :-      % Install from directory
  540    uri_file_name(FileURL, Dir),
  541    exists_directory(Dir),
  542    pack_info_term(Dir, name(Pack)),
  543    !,
  544    (   pack_info_term(Dir, version(Version))
  545    ->  uri_file_name(DirURL, Dir),
  546        Options = [url(DirURL), version(Version)]
  547    ;   throw(error(existence_error(key, version, Dir),_))
  548    ).
  549pack_default_options('.', Pack, _, Options) :-          % Install from CWD
  550    pack_info_term('.', name(Pack)),
  551    !,
  552    working_directory(Dir, Dir),
  553    (   pack_info_term(Dir, version(Version))
  554    ->  uri_file_name(DirURL, Dir),
  555        Options = [url(DirURL), version(Version) | Options1],
  556        (   current_prolog_flag(windows, true)
  557        ->  Options1 = []
  558        ;   Options1 = [link(true), rebuild(make)]
  559        )
  560    ;   throw(error(existence_error(key, version, Dir),_))
  561    ).
  562pack_default_options(URL, Pack, _, Options) :-          % Install from URL
  563    pack_version_file(Pack, Version, URL),
  564    download_url(URL),
  565    !,
  566    available_download_versions(URL, [URLVersion-LatestURL|_]),
  567    Options = [url(LatestURL)|VersionOptions],
  568    version_options(Version, URLVersion, VersionOptions).
  569pack_default_options(Pack, Pack, OptsIn, Options) :-    % Install from name
  570    \+ uri_is_global(Pack),                             % ignore URLs
  571    query_pack_server(locate(Pack), Reply, OptsIn),
  572    (   Reply = true(Results)
  573    ->  pack_select_candidate(Pack, Results, OptsIn, Options)
  574    ;   print_message(warning, pack(no_match(Pack))),
  575        fail
  576    ).
  577
  578version_options(Version, Version, [version(Version)]) :- !.
  579version_options(Version, _, [version(Version)]) :-
  580    Version = version(List),
  581    maplist(integer, List),
  582    !.
  583version_options(_, _, []).
  584
  585%!  pack_select_candidate(+Pack, +AvailableVersions, +OptionsIn, -Options)
  586%
  587%   Select from available packages.
  588
  589pack_select_candidate(Pack, [AtomVersion-_|_], Options,
  590                      [already_installed(pack(Pack, Installed))|Options]) :-
  591    current_pack(Pack),
  592    pack_info(Pack, _, version(InstalledAtom)),
  593    atom_version(InstalledAtom, Installed),
  594    atom_version(AtomVersion, Version),
  595    Installed @>= Version,
  596    !.
  597pack_select_candidate(Pack, Available, Options, OptsOut) :-
  598    option(url(URL), Options),
  599    memberchk(_Version-URLs, Available),
  600    memberchk(URL, URLs),
  601    !,
  602    (   git_url(URL, Pack)
  603    ->  Extra = [git(true)]
  604    ;   Extra = []
  605    ),
  606    OptsOut = [url(URL), inquiry(true) | Extra].
  607pack_select_candidate(Pack, [Version-[URL]|_], Options,
  608                      [url(URL), git(true), inquiry(true)]) :-
  609    git_url(URL, Pack),
  610    !,
  611    confirm(install_from(Pack, Version, git(URL)), yes, Options).
  612pack_select_candidate(Pack, [Version-[URL]|More], Options,
  613                      [url(URL), inquiry(true) | Upgrade]) :-
  614    (   More == []
  615    ->  !
  616    ;   true
  617    ),
  618    confirm(install_from(Pack, Version, URL), yes, Options),
  619    !,
  620    add_upgrade(Pack, Upgrade).
  621pack_select_candidate(Pack, [Version-URLs|_], Options,
  622                      [url(URL), inquiry(true)|Rest]) :-
  623    maplist(url_menu_item, URLs, Tagged),
  624    append(Tagged, [cancel=cancel], Menu),
  625    Menu = [Default=_|_],
  626    menu(pack(select_install_from(Pack, Version)),
  627         Menu, Default, Choice, Options),
  628    (   Choice == cancel
  629    ->  fail
  630    ;   Choice = git(URL)
  631    ->  Rest = [git(true)|Upgrade]
  632    ;   Choice = URL,
  633        Rest = Upgrade
  634    ),
  635    add_upgrade(Pack, Upgrade).
  636
  637add_upgrade(Pack, Options) :-
  638    current_pack(Pack),
  639    !,
  640    Options = [upgrade(true)].
  641add_upgrade(_, []).
  642
  643url_menu_item(URL, git(URL)=install_from(git(URL))) :-
  644    git_url(URL, _),
  645    !.
  646url_menu_item(URL, URL=install_from(URL)).
  647
  648
  649%!  pack_install(+Name, +Options) is det.
  650%
  651%   Install package Name.  Processes  the   options  below.  Default
  652%   options as would be used by  pack_install/1 are used to complete
  653%   the provided Options.
  654%
  655%     * url(+URL)
  656%     Source for downloading the package
  657%     * package_directory(+Dir)
  658%     Directory into which to install the package.
  659%     * global(+Boolean)
  660%     If `true`, install in the XDG common application data path, making
  661%     the pack accessible to everyone. If `false`, install in the XDG
  662%     user application data path, making the pack accessible for the
  663%     current user only.  If the option is absent, use the first
  664%     existing and writable directory.  If that doesn't exist find
  665%     locations where it can be created and prompt the user to do
  666%     so.
  667%     * interactive(+Boolean)
  668%     Use default answer without asking the user if there
  669%     is a default action.
  670%     * silent(+Boolean)
  671%     If `true` (default false), suppress informational progress
  672%     messages.
  673%     * upgrade(+Boolean)
  674%     If `true` (default `false`), upgrade package if it is already
  675%     installed.
  676%     * rebuild(Condition)
  677%     Rebuild the foreign components.  Condition is one of
  678%     `if_absent` (default, do nothing if the directory with foreign
  679%     resources exists), `make` (run `make`) or `true` (run `make
  680%     distclean` followed by the default configure and build steps).
  681%     * test(Boolean)
  682%     If `true` (default), run the pack tests.
  683%     * git(+Boolean)
  684%     If `true` (default `false` unless `URL` ends with =.git=),
  685%     assume the URL is a GIT repository.
  686%     * link(+Boolean)
  687%     Can be used if the installation source is a local directory
  688%     and the file system supports symbolic links.  In this case
  689%     the system adds the current directory to the pack registration
  690%     using a symbolic link and performs the local installation steps.
  691%
  692%   Non-interactive installation can be established using the option
  693%   interactive(false). It is adviced to   install from a particular
  694%   _trusted_ URL instead of the  plain   pack  name  for unattented
  695%   operation.
  696
  697pack_install(Spec, Options) :-
  698    pack_default_options(Spec, Pack, Options, DefOptions),
  699    (   option(already_installed(Installed), DefOptions)
  700    ->  print_message(informational, pack(already_installed(Installed)))
  701    ;   merge_options(Options, DefOptions, PackOptions),
  702        update_dependency_db,
  703        pack_install_dir(PackDir, PackOptions),
  704        pack_install(Pack, PackDir, PackOptions)
  705    ).
  706
  707pack_install_dir(PackDir, Options) :-
  708    option(package_directory(PackDir), Options),
  709    !.
  710pack_install_dir(PackDir, Options) :-
  711    base_alias(Alias, Options),
  712    absolute_file_name(Alias, PackDir,
  713                       [ file_type(directory),
  714                         access(write),
  715                         file_errors(fail)
  716                       ]),
  717    !.
  718pack_install_dir(PackDir, Options) :-
  719    pack_create_install_dir(PackDir, Options).
  720
  721base_alias(Alias, Options) :-
  722    option(global(true), Options),
  723    !,
  724    Alias = common_app_data(pack).
  725base_alias(Alias, Options) :-
  726    option(global(false), Options),
  727    !,
  728    Alias = user_app_data(pack).
  729base_alias(Alias, _Options) :-
  730    Alias = pack('.').
  731
  732pack_create_install_dir(PackDir, Options) :-
  733    base_alias(Alias, Options),
  734    findall(Candidate = create_dir(Candidate),
  735            ( absolute_file_name(Alias, Candidate, [solutions(all)]),
  736              \+ exists_file(Candidate),
  737              \+ exists_directory(Candidate),
  738              file_directory_name(Candidate, Super),
  739              (   exists_directory(Super)
  740              ->  access_file(Super, write)
  741              ;   true
  742              )
  743            ),
  744            Candidates0),
  745    list_to_set(Candidates0, Candidates),   % keep order
  746    pack_create_install_dir(Candidates, PackDir, Options).
  747
  748pack_create_install_dir(Candidates, PackDir, Options) :-
  749    Candidates = [Default=_|_],
  750    !,
  751    append(Candidates, [cancel=cancel], Menu),
  752    menu(pack(create_pack_dir), Menu, Default, Selected, Options),
  753    Selected \== cancel,
  754    (   catch(make_directory_path(Selected), E,
  755              (print_message(warning, E), fail))
  756    ->  PackDir = Selected
  757    ;   delete(Candidates, PackDir=create_dir(PackDir), Remaining),
  758        pack_create_install_dir(Remaining, PackDir, Options)
  759    ).
  760pack_create_install_dir(_, _, _) :-
  761    print_message(error, pack(cannot_create_dir(pack(.)))),
  762    fail.
  763
  764
  765%!  pack_install(+Pack, +PackDir, +Options)
  766%
  767%   Install package Pack into PackDir.  Options:
  768%
  769%     - url(URL)
  770%     Install from the given URL, URL is either a file://, a git URL
  771%     or a download URL.
  772%     - upgrade(Boolean)
  773%     If Pack is already installed and Boolean is `true`, update the
  774%     package to the latest version.  If Boolean is `false` print
  775%     an error and fail.
  776
  777pack_install(Name, _, Options) :-
  778    current_pack(Name, Dir),
  779    option(upgrade(false), Options, false),
  780    \+ pack_is_in_local_dir(Name, Dir, Options),
  781    print_message(error, pack(already_installed(Name))),
  782    pack_info(Name),
  783    print_message(information, pack(remove_with(Name))),
  784    !,
  785    fail.
  786pack_install(Name, PackDir, Options) :-
  787    option(url(URL), Options),
  788    uri_file_name(URL, Source),
  789    !,
  790    pack_install_from_local(Source, PackDir, Name, Options).
  791pack_install(Name, PackDir, Options) :-
  792    option(url(URL), Options),
  793    uri_components(URL, Components),
  794    uri_data(scheme, Components, Scheme),
  795    pack_install_from_url(Scheme, URL, PackDir, Name, Options).
  796
  797%!  pack_install_from_local(+Source, +PackTopDir, +Name, +Options)
  798%
  799%   Install a package from a local media.
  800%
  801%   @tbd    Provide an option to install directories using a
  802%           link (or file-links).
  803
  804pack_install_from_local(Source, PackTopDir, Name, Options) :-
  805    exists_directory(Source),
  806    !,
  807    directory_file_path(PackTopDir, Name, PackDir),
  808    (   option(link(true), Options)
  809    ->  (   same_file(Source, PackDir)
  810        ->  true
  811        ;   atom_concat(PackTopDir, '/', PackTopDirS),
  812            relative_file_name(Source, PackTopDirS, RelPath),
  813            link_file(RelPath, PackDir, symbolic),
  814            assertion(same_file(Source, PackDir))
  815        )
  816    ;   prepare_pack_dir(PackDir, Options),
  817        copy_directory(Source, PackDir)
  818    ),
  819    pack_post_install(Name, PackDir, Options).
  820pack_install_from_local(Source, PackTopDir, Name, Options) :-
  821    exists_file(Source),
  822    directory_file_path(PackTopDir, Name, PackDir),
  823    prepare_pack_dir(PackDir, Options),
  824    pack_unpack(Source, PackDir, Name, Options),
  825    pack_post_install(Name, PackDir, Options).
  826
  827pack_is_in_local_dir(_Pack, PackDir, Options) :-
  828    option(url(DirURL), Options),
  829    uri_file_name(DirURL, Dir),
  830    same_file(PackDir, Dir).
  831
  832
  833%!  pack_unpack(+SourceFile, +PackDir, +Pack, +Options)
  834%
  835%   Unpack an archive to the given package dir.
  836
  837:- if(exists_source(library(archive))).  838pack_unpack(Source, PackDir, Pack, Options) :-
  839    ensure_loaded_archive,
  840    pack_archive_info(Source, Pack, _Info, StripOptions),
  841    prepare_pack_dir(PackDir, Options),
  842    archive_extract(Source, PackDir,
  843                    [ exclude(['._*'])          % MacOS resource forks
  844                    | StripOptions
  845                    ]).
  846:- else.  847pack_unpack(_,_,_,_) :-
  848    existence_error(library, archive).
  849:- endif.  850
  851                 /*******************************
  852                 *             INFO             *
  853                 *******************************/
  854
  855%!  pack_archive_info(+Archive, +Pack, -Info, -Strip)
  856%
  857%   True when Archive archives Pack. Info  is unified with the terms
  858%   from pack.pl in the  pack  and   Strip  is  the strip-option for
  859%   archive_extract/3.
  860%
  861%   Requires library(archive), which is lazily loaded when needed.
  862%
  863%   @error  existence_error(pack_file, 'pack.pl') if the archive
  864%           doesn't contain pack.pl
  865%   @error  Syntax errors if pack.pl cannot be parsed.
  866
  867:- if(exists_source(library(archive))).  868ensure_loaded_archive :-
  869    current_predicate(archive_open/3),
  870    !.
  871ensure_loaded_archive :-
  872    use_module(library(archive)).
  873
  874pack_archive_info(Archive, Pack, [archive_size(Bytes)|Info], Strip) :-
  875    ensure_loaded_archive,
  876    size_file(Archive, Bytes),
  877    setup_call_cleanup(
  878        archive_open(Archive, Handle, []),
  879        (   repeat,
  880            (   archive_next_header(Handle, InfoFile)
  881            ->  true
  882            ;   !, fail
  883            )
  884        ),
  885        archive_close(Handle)),
  886    file_base_name(InfoFile, 'pack.pl'),
  887    atom_concat(Prefix, 'pack.pl', InfoFile),
  888    strip_option(Prefix, Pack, Strip),
  889    setup_call_cleanup(
  890        archive_open_entry(Handle, Stream),
  891        read_stream_to_terms(Stream, Info),
  892        close(Stream)),
  893    !,
  894    must_be(ground, Info),
  895    maplist(valid_info_term, Info).
  896:- else.  897pack_archive_info(_, _, _, _) :-
  898    existence_error(library, archive).
  899:- endif.  900pack_archive_info(_, _, _, _) :-
  901    existence_error(pack_file, 'pack.pl').
  902
  903strip_option('', _, []) :- !.
  904strip_option('./', _, []) :- !.
  905strip_option(Prefix, Pack, [remove_prefix(Prefix)]) :-
  906    atom_concat(PrefixDir, /, Prefix),
  907    file_base_name(PrefixDir, Base),
  908    (   Base == Pack
  909    ->  true
  910    ;   pack_version_file(Pack, _, Base)
  911    ->  true
  912    ;   \+ sub_atom(PrefixDir, _, _, _, /)
  913    ).
  914
  915read_stream_to_terms(Stream, Terms) :-
  916    read(Stream, Term0),
  917    read_stream_to_terms(Term0, Stream, Terms).
  918
  919read_stream_to_terms(end_of_file, _, []) :- !.
  920read_stream_to_terms(Term0, Stream, [Term0|Terms]) :-
  921    read(Stream, Term1),
  922    read_stream_to_terms(Term1, Stream, Terms).
  923
  924
  925%!  pack_git_info(+GitDir, -Hash, -Info) is det.
  926%
  927%   Retrieve info from a cloned git   repository  that is compatible
  928%   with pack_archive_info/4.
  929
  930pack_git_info(GitDir, Hash, [git(true), installed_size(Bytes)|Info]) :-
  931    exists_directory(GitDir),
  932    !,
  933    git_ls_tree(Entries, [directory(GitDir)]),
  934    git_hash(Hash, [directory(GitDir)]),
  935    maplist(arg(4), Entries, Sizes),
  936    sum_list(Sizes, Bytes),
  937    directory_file_path(GitDir, 'pack.pl', InfoFile),
  938    read_file_to_terms(InfoFile, Info, [encoding(utf8)]),
  939    must_be(ground, Info),
  940    maplist(valid_info_term, Info).
  941
  942%!  download_file_sanity_check(+Archive, +Pack, +Info) is semidet.
  943%
  944%   Perform basic sanity checks on DownloadFile
  945
  946download_file_sanity_check(Archive, Pack, Info) :-
  947    info_field(name(Name), Info),
  948    info_field(version(VersionAtom), Info),
  949    atom_version(VersionAtom, Version),
  950    pack_version_file(PackA, VersionA, Archive),
  951    must_match([Pack, PackA, Name], name),
  952    must_match([Version, VersionA], version).
  953
  954info_field(Field, Info) :-
  955    memberchk(Field, Info),
  956    ground(Field),
  957    !.
  958info_field(Field, _Info) :-
  959    functor(Field, FieldName, _),
  960    print_message(error, pack(missing(FieldName))),
  961    fail.
  962
  963must_match(Values, _Field) :-
  964    sort(Values, [_]),
  965    !.
  966must_match(Values, Field) :-
  967    print_message(error, pack(conflict(Field, Values))),
  968    fail.
  969
  970
  971                 /*******************************
  972                 *         INSTALLATION         *
  973                 *******************************/
  974
  975%!  prepare_pack_dir(+Dir, +Options)
  976%
  977%   Prepare for installing the package into  Dir. This
  978%
  979%     - If the directory exist and is empty, done.
  980%     - Else if the directory exists, remove the directory and recreate
  981%       it. Note that if the directory is a symlink this just deletes
  982%       the link.
  983%     - Else create the directory.
  984
  985prepare_pack_dir(Dir, Options) :-
  986    exists_directory(Dir),
  987    !,
  988    (   empty_directory(Dir)
  989    ->  true
  990    ;   (   option(upgrade(true), Options)
  991        ;   confirm(remove_existing_pack(Dir), yes, Options)
  992        )
  993    ->  delete_directory_and_contents(Dir),
  994        make_directory(Dir)
  995    ).
  996prepare_pack_dir(Dir, _) :-
  997    make_directory(Dir).
  998
  999%!  empty_directory(+Directory) is semidet.
 1000%
 1001%   True if Directory is empty (holds no files or sub-directories).
 1002
 1003empty_directory(Dir) :-
 1004    \+ ( directory_files(Dir, Entries),
 1005         member(Entry, Entries),
 1006         \+ special(Entry)
 1007       ).
 1008
 1009special(.).
 1010special(..).
 1011
 1012
 1013%!  pack_install_from_url(+Scheme, +URL, +PackDir, +Pack, +Options)
 1014%
 1015%   Install a package from a remote source. For git repositories, we
 1016%   simply clone. Archives are  downloaded.   We  currently  use the
 1017%   built-in HTTP client. For complete  coverage, we should consider
 1018%   using an external (e.g., curl) if available.
 1019
 1020pack_install_from_url(_, URL, PackTopDir, Pack, Options) :-
 1021    option(git(true), Options),
 1022    !,
 1023    directory_file_path(PackTopDir, Pack, PackDir),
 1024    prepare_pack_dir(PackDir, Options),
 1025    run_process(path(git), [clone, URL, PackDir], []),
 1026    pack_git_info(PackDir, Hash, Info),
 1027    pack_inquiry(URL, git(Hash), Info, Options),
 1028    show_info(Pack, Info, Options),
 1029    confirm(git_post_install(PackDir, Pack), yes, Options),
 1030    pack_post_install(Pack, PackDir, Options).
 1031pack_install_from_url(Scheme, URL, PackTopDir, Pack, Options) :-
 1032    download_scheme(Scheme),
 1033    directory_file_path(PackTopDir, Pack, PackDir),
 1034    prepare_pack_dir(PackDir, Options),
 1035    pack_download_dir(PackTopDir, DownLoadDir),
 1036    download_file(URL, Pack, DownloadBase, Options),
 1037    directory_file_path(DownLoadDir, DownloadBase, DownloadFile),
 1038    setup_call_cleanup(
 1039        http_open(URL, In,
 1040                  [ cert_verify_hook(ssl_verify)
 1041                  ]),
 1042        setup_call_cleanup(
 1043            open(DownloadFile, write, Out, [type(binary)]),
 1044            copy_stream_data(In, Out),
 1045            close(Out)),
 1046        close(In)),
 1047    pack_archive_info(DownloadFile, Pack, Info, _),
 1048    download_file_sanity_check(DownloadFile, Pack, Info),
 1049    pack_inquiry(URL, DownloadFile, Info, Options),
 1050    show_info(Pack, Info, Options),
 1051    confirm(install_downloaded(DownloadFile), yes, Options),
 1052    pack_install_from_local(DownloadFile, PackTopDir, Pack, Options).
 1053
 1054%!  download_file(+URL, +Pack, -File, +Options) is det.
 1055
 1056download_file(URL, Pack, File, Options) :-
 1057    option(version(Version), Options),
 1058    !,
 1059    atom_version(VersionA, Version),
 1060    file_name_extension(_, Ext, URL),
 1061    format(atom(File), '~w-~w.~w', [Pack, VersionA, Ext]).
 1062download_file(URL, Pack, File, _) :-
 1063    file_base_name(URL,Basename),
 1064    no_int_file_name_extension(Tag,Ext,Basename),
 1065    tag_version(Tag,Version),
 1066    !,
 1067    atom_version(VersionA,Version),
 1068    format(atom(File0), '~w-~w', [Pack, VersionA]),
 1069    file_name_extension(File0, Ext, File).
 1070download_file(URL, _, File, _) :-
 1071    file_base_name(URL, File).
 1072
 1073%!  pack_url_file(+URL, -File) is det.
 1074%
 1075%   True if File is a unique id for the referenced pack and version.
 1076%   Normally, that is simply the  base   name,  but  GitHub archives
 1077%   destroy this picture. Needed by the pack manager.
 1078
 1079pack_url_file(URL, FileID) :-
 1080    github_release_url(URL, Pack, Version),
 1081    !,
 1082    download_file(URL, Pack, FileID, [version(Version)]).
 1083pack_url_file(URL, FileID) :-
 1084    file_base_name(URL, FileID).
 1085
 1086
 1087:- public ssl_verify/5. 1088
 1089%!  ssl_verify(+SSL, +ProblemCert, +AllCerts, +FirstCert, +Error)
 1090%
 1091%   Currently we accept  all  certificates.   We  organise  our  own
 1092%   security using SHA1 signatures, so  we   do  not  care about the
 1093%   source of the data.
 1094
 1095ssl_verify(_SSL,
 1096           _ProblemCertificate, _AllCertificates, _FirstCertificate,
 1097           _Error).
 1098
 1099pack_download_dir(PackTopDir, DownLoadDir) :-
 1100    directory_file_path(PackTopDir, 'Downloads', DownLoadDir),
 1101    (   exists_directory(DownLoadDir)
 1102    ->  true
 1103    ;   make_directory(DownLoadDir)
 1104    ),
 1105    (   access_file(DownLoadDir, write)
 1106    ->  true
 1107    ;   permission_error(write, directory, DownLoadDir)
 1108    ).
 1109
 1110%!  download_url(+URL) is det.
 1111%
 1112%   True if URL looks like a URL we can download from.
 1113
 1114download_url(URL) :-
 1115    atom(URL),
 1116    uri_components(URL, Components),
 1117    uri_data(scheme, Components, Scheme),
 1118    download_scheme(Scheme).
 1119
 1120download_scheme(http).
 1121download_scheme(https) :-
 1122    catch(use_module(library(http/http_ssl_plugin)),
 1123          E, (print_message(warning, E), fail)).
 1124
 1125%!  pack_post_install(+Pack, +PackDir, +Options) is det.
 1126%
 1127%   Process post installation work.  Steps:
 1128%
 1129%     - Create foreign resources
 1130%     - Register directory as autoload library
 1131%     - Attach the package
 1132
 1133pack_post_install(Pack, PackDir, Options) :-
 1134    post_install_foreign(Pack, PackDir, Options),
 1135    post_install_autoload(PackDir, Options),
 1136    '$pack_attach'(PackDir).
 1137
 1138%!  pack_rebuild(+Pack) is det.
 1139%
 1140%   Rebuild possible foreign components of Pack.
 1141
 1142pack_rebuild(Pack) :-
 1143    current_pack(Pack, PackDir),
 1144    !,
 1145    post_install_foreign(Pack, PackDir, [rebuild(true)]).
 1146pack_rebuild(Pack) :-
 1147    unattached_pacth(Pack, PackDir),
 1148    !,
 1149    post_install_foreign(Pack, PackDir, [rebuild(true)]).
 1150pack_rebuild(Pack) :-
 1151    existence_error(pack, Pack).
 1152
 1153unattached_pacth(Pack, BaseDir) :-
 1154    directory_file_path(Pack, 'pack.pl', PackFile),
 1155    absolute_file_name(pack(PackFile), PackPath,
 1156                       [ access(read),
 1157                         file_errors(fail)
 1158                       ]),
 1159    file_directory_name(PackPath, BaseDir).
 1160
 1161%!  pack_rebuild is det.
 1162%
 1163%   Rebuild foreign components of all packages.
 1164
 1165pack_rebuild :-
 1166    forall(current_pack(Pack),
 1167           ( print_message(informational, pack(rebuild(Pack))),
 1168             pack_rebuild(Pack)
 1169           )).
 1170
 1171
 1172%!  post_install_foreign(+Pack, +PackDir, +Options) is det.
 1173%
 1174%   Install foreign parts of the package.
 1175
 1176post_install_foreign(Pack, PackDir, Options) :-
 1177    is_foreign_pack(PackDir, _),
 1178    !,
 1179    (   pack_info_term(PackDir, pack_version(Version))
 1180    ->  true
 1181    ;   Version = 1
 1182    ),
 1183    option(rebuild(Rebuild), Options, if_absent),
 1184    (   Rebuild == if_absent,
 1185        foreign_present(PackDir)
 1186    ->  print_message(informational, pack(kept_foreign(Pack)))
 1187    ;   BuildSteps0 = [[dependencies], [configure], build, [test], install],
 1188        (   Rebuild == true
 1189        ->  BuildSteps1 = [distclean|BuildSteps0]
 1190        ;   BuildSteps1 = BuildSteps0
 1191        ),
 1192        (   option(test(false), Options)
 1193        ->  delete(BuildSteps1, [test], BuildSteps)
 1194        ;   BuildSteps = BuildSteps1
 1195        ),
 1196        build_steps(BuildSteps, PackDir, [pack_version(Version)|Options])
 1197    ).
 1198post_install_foreign(_, _, _).
 1199
 1200
 1201%!  foreign_present(+PackDir) is semidet.
 1202%
 1203%   True if we find one or more modules  in the pack `lib` directory for
 1204%   the current architecture. Does not check   that these can be loaded,
 1205%   nor whether all required modules are present.
 1206
 1207foreign_present(PackDir) :-
 1208    current_prolog_flag(arch, Arch),
 1209    atomic_list_concat([PackDir, '/lib'], ForeignBaseDir),
 1210    exists_directory(ForeignBaseDir),
 1211    !,
 1212    atomic_list_concat([PackDir, '/lib/', Arch], ForeignDir),
 1213    exists_directory(ForeignDir),
 1214    current_prolog_flag(shared_object_extension, Ext),
 1215    atomic_list_concat([ForeignDir, '/*.', Ext], Pattern),
 1216    expand_file_name(Pattern, Files),
 1217    Files \== [].
 1218
 1219%!  is_foreign_pack(+PackDir, -Type) is nondet.
 1220%
 1221%   True when PackDir contains  files  that   indicate  the  need  for a
 1222%   specific class of build tools indicated by Type.
 1223
 1224is_foreign_pack(PackDir, Type) :-
 1225    foreign_file(File, Type),
 1226    directory_file_path(PackDir, File, Path),
 1227    exists_file(Path).
 1228
 1229foreign_file('CMakeLists.txt', cmake).
 1230foreign_file('configure',      configure).
 1231foreign_file('configure.in',   autoconf).
 1232foreign_file('configure.ac',   autoconf).
 1233foreign_file('Makefile.am',    automake).
 1234foreign_file('Makefile',       make).
 1235foreign_file('makefile',       make).
 1236foreign_file('conanfile.txt',  conan).
 1237foreign_file('conanfile.py',   conan).
 1238
 1239
 1240                 /*******************************
 1241                 *           AUTOLOAD           *
 1242                 *******************************/
 1243
 1244%!  post_install_autoload(+PackDir, +Options)
 1245%
 1246%   Create an autoload index if the package demands such.
 1247
 1248post_install_autoload(PackDir, Options) :-
 1249    option(autoload(true), Options, true),
 1250    pack_info_term(PackDir, autoload(true)),
 1251    !,
 1252    directory_file_path(PackDir, prolog, PrologLibDir),
 1253    make_library_index(PrologLibDir).
 1254post_install_autoload(_, _).
 1255
 1256
 1257                 /*******************************
 1258                 *            UPGRADE           *
 1259                 *******************************/
 1260
 1261%!  pack_upgrade(+Pack) is semidet.
 1262%
 1263%   Try to upgrade the package Pack.
 1264%
 1265%   @tbd    Update dependencies when updating a pack from git?
 1266
 1267pack_upgrade(Pack) :-
 1268    pack_info(Pack, _, directory(Dir)),
 1269    directory_file_path(Dir, '.git', GitDir),
 1270    exists_directory(GitDir),
 1271    !,
 1272    print_message(informational, pack(git_fetch(Dir))),
 1273    git([fetch], [ directory(Dir) ]),
 1274    git_describe(V0, [ directory(Dir) ]),
 1275    git_describe(V1, [ directory(Dir), commit('origin/master') ]),
 1276    (   V0 == V1
 1277    ->  print_message(informational, pack(up_to_date(Pack)))
 1278    ;   confirm(upgrade(Pack, V0, V1), yes, []),
 1279        git([merge, 'origin/master'], [ directory(Dir) ]),
 1280        pack_rebuild(Pack)
 1281    ).
 1282pack_upgrade(Pack) :-
 1283    once(pack_info(Pack, _, version(VersionAtom))),
 1284    atom_version(VersionAtom, Version),
 1285    pack_info(Pack, _, download(URL)),
 1286    (   wildcard_pattern(URL)
 1287    ->  true
 1288    ;   github_url(URL, _User, _Repo)
 1289    ),
 1290    !,
 1291    available_download_versions(URL, [Latest-LatestURL|_Versions]),
 1292    (   Latest @> Version
 1293    ->  confirm(upgrade(Pack, Version, Latest), yes, []),
 1294        pack_install(Pack,
 1295                     [ url(LatestURL),
 1296                       upgrade(true),
 1297                       pack(Pack)
 1298                     ])
 1299    ;   print_message(informational, pack(up_to_date(Pack)))
 1300    ).
 1301pack_upgrade(Pack) :-
 1302    print_message(warning, pack(no_upgrade_info(Pack))).
 1303
 1304
 1305                 /*******************************
 1306                 *            REMOVE            *
 1307                 *******************************/
 1308
 1309%!  pack_remove(+Name) is det.
 1310%
 1311%   Remove the indicated package.
 1312
 1313pack_remove(Pack) :-
 1314    update_dependency_db,
 1315    (   setof(Dep, pack_depends_on(Dep, Pack), Deps)
 1316    ->  confirm_remove(Pack, Deps, Delete),
 1317        forall(member(P, Delete), pack_remove_forced(P))
 1318    ;   pack_remove_forced(Pack)
 1319    ).
 1320
 1321pack_remove_forced(Pack) :-
 1322    catch('$pack_detach'(Pack, BaseDir),
 1323          error(existence_error(pack, Pack), _),
 1324          fail),
 1325    !,
 1326    print_message(informational, pack(remove(BaseDir))),
 1327    delete_directory_and_contents(BaseDir).
 1328pack_remove_forced(Pack) :-
 1329    unattached_pacth(Pack, BaseDir),
 1330    !,
 1331    delete_directory_and_contents(BaseDir).
 1332pack_remove_forced(Pack) :-
 1333    print_message(informational, error(existence_error(pack, Pack),_)).
 1334
 1335confirm_remove(Pack, Deps, Delete) :-
 1336    print_message(warning, pack(depends(Pack, Deps))),
 1337    menu(pack(resolve_remove),
 1338         [ [Pack]      = remove_only(Pack),
 1339           [Pack|Deps] = remove_deps(Pack, Deps),
 1340           []          = cancel
 1341         ], [], Delete, []),
 1342    Delete \== [].
 1343
 1344
 1345                 /*******************************
 1346                 *           PROPERTIES         *
 1347                 *******************************/
 1348
 1349%!  pack_property(?Pack, ?Property) is nondet.
 1350%
 1351%   True when Property  is  a  property   of  an  installed  Pack.  This
 1352%   interface is intended for programs that   wish  to interact with the
 1353%   package manager. Defined properties are:
 1354%
 1355%     - directory(Directory)
 1356%     Directory into which the package is installed
 1357%     - version(Version)
 1358%     Installed version
 1359%     - title(Title)
 1360%     Full title of the package
 1361%     - author(Author)
 1362%     Registered author
 1363%     - download(URL)
 1364%     Official download URL
 1365%     - readme(File)
 1366%     Package README file (if present)
 1367%     - todo(File)
 1368%     Package TODO file (if present)
 1369
 1370pack_property(Pack, Property) :-
 1371    findall(Pack-Property, pack_property_(Pack, Property), List),
 1372    member(Pack-Property, List).            % make det if applicable
 1373
 1374pack_property_(Pack, Property) :-
 1375    pack_info(Pack, _, Property).
 1376pack_property_(Pack, Property) :-
 1377    \+ \+ info_file(Property, _),
 1378    '$pack':pack(Pack, BaseDir),
 1379    access_file(BaseDir, read),
 1380    directory_files(BaseDir, Files),
 1381    member(File, Files),
 1382    info_file(Property, Pattern),
 1383    downcase_atom(File, Pattern),
 1384    directory_file_path(BaseDir, File, InfoFile),
 1385    arg(1, Property, InfoFile).
 1386
 1387info_file(readme(_), 'readme.txt').
 1388info_file(readme(_), 'readme').
 1389info_file(todo(_),   'todo.txt').
 1390info_file(todo(_),   'todo').
 1391
 1392
 1393                 /*******************************
 1394                 *             GIT              *
 1395                 *******************************/
 1396
 1397%!  git_url(+URL, -Pack) is semidet.
 1398%
 1399%   True if URL describes a git url for Pack
 1400
 1401git_url(URL, Pack) :-
 1402    uri_components(URL, Components),
 1403    uri_data(scheme, Components, Scheme),
 1404    nonvar(Scheme),                         % must be full URL
 1405    uri_data(path, Components, Path),
 1406    (   Scheme == git
 1407    ->  true
 1408    ;   git_download_scheme(Scheme),
 1409        file_name_extension(_, git, Path)
 1410    ;   git_download_scheme(Scheme),
 1411        catch(git_ls_remote(URL, _, [refs(['HEAD']), error(_)]), _, fail)
 1412    ->  true
 1413    ),
 1414    file_base_name(Path, PackExt),
 1415    (   file_name_extension(Pack, git, PackExt)
 1416    ->  true
 1417    ;   Pack = PackExt
 1418    ),
 1419    (   safe_pack_name(Pack)
 1420    ->  true
 1421    ;   domain_error(pack_name, Pack)
 1422    ).
 1423
 1424git_download_scheme(http).
 1425git_download_scheme(https).
 1426
 1427%!  safe_pack_name(+Name:atom) is semidet.
 1428%
 1429%   Verifies that Name is a valid   pack  name. This avoids trickery
 1430%   with pack file names to make shell commands behave unexpectly.
 1431
 1432safe_pack_name(Name) :-
 1433    atom_length(Name, Len),
 1434    Len >= 3,                               % demand at least three length
 1435    atom_codes(Name, Codes),
 1436    maplist(safe_pack_char, Codes),
 1437    !.
 1438
 1439safe_pack_char(C) :- between(0'a, 0'z, C), !.
 1440safe_pack_char(C) :- between(0'A, 0'Z, C), !.
 1441safe_pack_char(C) :- between(0'0, 0'9, C), !.
 1442safe_pack_char(0'_).
 1443
 1444
 1445                 /*******************************
 1446                 *         VERSION LOGIC        *
 1447                 *******************************/
 1448
 1449%!  pack_version_file(-Pack, -Version, +File) is semidet.
 1450%
 1451%   True if File is the  name  of  a   file  or  URL  of a file that
 1452%   contains Pack at Version. File must   have  an extension and the
 1453%   basename  must  be  of   the    form   <pack>-<n>{.<m>}*.  E.g.,
 1454%   =|mypack-1.5|=.
 1455
 1456pack_version_file(Pack, Version, GitHubRelease) :-
 1457    atomic(GitHubRelease),
 1458    github_release_url(GitHubRelease, Pack, Version),
 1459    !.
 1460pack_version_file(Pack, Version, Path) :-
 1461    atomic(Path),
 1462    file_base_name(Path, File),
 1463    no_int_file_name_extension(Base, _Ext, File),
 1464    atom_codes(Base, Codes),
 1465    (   phrase(pack_version(Pack, Version), Codes),
 1466        safe_pack_name(Pack)
 1467    ->  true
 1468    ).
 1469
 1470no_int_file_name_extension(Base, Ext, File) :-
 1471    file_name_extension(Base0, Ext0, File),
 1472    \+ atom_number(Ext0, _),
 1473    !,
 1474    Base = Base0,
 1475    Ext = Ext0.
 1476no_int_file_name_extension(File, '', File).
 1477
 1478
 1479
 1480%!  github_release_url(+URL, -Pack, -Version) is semidet.
 1481%
 1482%   True when URL is the URL of a GitHub release.  Such releases are
 1483%   accessible as
 1484%
 1485%     ==
 1486%     https:/github.com/<owner>/<pack>/archive/[vV]?<version>.zip'
 1487%     ==
 1488
 1489github_release_url(URL, Pack, Version) :-
 1490    uri_components(URL, Components),
 1491    uri_data(authority, Components, 'github.com'),
 1492    uri_data(scheme, Components, Scheme),
 1493    download_scheme(Scheme),
 1494    uri_data(path, Components, Path),
 1495    github_archive_path(Archive,Pack,File),
 1496    atomic_list_concat(Archive, /, Path),
 1497    file_name_extension(Tag, Ext, File),
 1498    github_archive_extension(Ext),
 1499    tag_version(Tag, Version),
 1500    !.
 1501
 1502github_archive_path(['',_User,Pack,archive,File],Pack,File).
 1503github_archive_path(['',_User,Pack,archive,refs,tags,File],Pack,File).
 1504
 1505github_archive_extension(tgz).
 1506github_archive_extension(zip).
 1507
 1508tag_version(Tag, Version) :-
 1509    version_tag_prefix(Prefix),
 1510    atom_concat(Prefix, AtomVersion, Tag),
 1511    atom_version(AtomVersion, Version).
 1512
 1513version_tag_prefix(v).
 1514version_tag_prefix('V').
 1515version_tag_prefix('').
 1516
 1517
 1518:- public
 1519    atom_version/2. 1520
 1521%!  atom_version(?Atom, ?Version)
 1522%
 1523%   Translate   between   atomic   version   representation   and   term
 1524%   representation.  The  term  representation  is  a  list  of  version
 1525%   components as integers and can be compared using `@>`
 1526
 1527atom_version(Atom, version(Parts)) :-
 1528    (   atom(Atom)
 1529    ->  atom_codes(Atom, Codes),
 1530        phrase(version(Parts), Codes)
 1531    ;   atomic_list_concat(Parts, '.', Atom)
 1532    ).
 1533
 1534pack_version(Pack, version(Parts)) -->
 1535    string(Codes), "-",
 1536    version(Parts),
 1537    !,
 1538    { atom_codes(Pack, Codes)
 1539    }.
 1540
 1541version([_|T]) -->
 1542    "*",
 1543    !,
 1544    (   "."
 1545    ->  version(T)
 1546    ;   []
 1547    ).
 1548version([H|T]) -->
 1549    integer(H),
 1550    (   "."
 1551    ->  version(T)
 1552    ;   { T = [] }
 1553    ).
 1554
 1555                 /*******************************
 1556                 *       QUERY CENTRAL DB       *
 1557                 *******************************/
 1558
 1559%!  pack_inquiry(+URL, +DownloadFile, +Info, +Options) is semidet.
 1560%
 1561%   Query the status of a package  with   the  central repository. To do
 1562%   this, we POST a Prolog document  containing   the  URL, info and the
 1563%   SHA1 hash to http://www.swi-prolog.org/pack/eval. The server replies
 1564%   using a list of Prolog terms, described  below. The only member that
 1565%   is always included is downloads (with default value 0).
 1566%
 1567%     - alt_hash(Count, URLs, Hash)
 1568%       A file with the same base-name, but a different hash was
 1569%       found at URLs and downloaded Count times.
 1570%     - downloads(Count)
 1571%       Number of times a file with this hash was downloaded.
 1572%     - rating(VoteCount, Rating)
 1573%       User rating (1..5), provided based on VoteCount votes.
 1574%     - dependency(Token, Pack, Version, URLs, SubDeps)
 1575%       Required tokens can be provided by the given provides.
 1576
 1577pack_inquiry(_, _, _, Options) :-
 1578    option(inquiry(false), Options),
 1579    !.
 1580pack_inquiry(URL, DownloadFile, Info, Options) :-
 1581    setting(server, ServerBase),
 1582    ServerBase \== '',
 1583    atom_concat(ServerBase, query, Server),
 1584    (   option(inquiry(true), Options)
 1585    ->  true
 1586    ;   confirm(inquiry(Server), yes, Options)
 1587    ),
 1588    !,
 1589    (   DownloadFile = git(SHA1)
 1590    ->  true
 1591    ;   file_sha1(DownloadFile, SHA1)
 1592    ),
 1593    query_pack_server(install(URL, SHA1, Info), Reply, Options),
 1594    inquiry_result(Reply, URL, Options).
 1595pack_inquiry(_, _, _, _).
 1596
 1597
 1598%!  query_pack_server(+Query, -Result, +Options)
 1599%
 1600%   Send a Prolog query  to  the   package  server  and  process its
 1601%   results.
 1602
 1603query_pack_server(Query, Result, Options) :-
 1604    setting(server, ServerBase),
 1605    ServerBase \== '',
 1606    atom_concat(ServerBase, query, Server),
 1607    format(codes(Data), '~q.~n', Query),
 1608    info_level(Informational, Options),
 1609    print_message(Informational, pack(contacting_server(Server))),
 1610    setup_call_cleanup(
 1611        http_open(Server, In,
 1612                  [ post(codes(application/'x-prolog', Data)),
 1613                    header(content_type, ContentType)
 1614                  ]),
 1615        read_reply(ContentType, In, Result),
 1616        close(In)),
 1617    message_severity(Result, Level, Informational),
 1618    print_message(Level, pack(server_reply(Result))).
 1619
 1620read_reply(ContentType, In, Result) :-
 1621    sub_atom(ContentType, 0, _, _, 'application/x-prolog'),
 1622    !,
 1623    set_stream(In, encoding(utf8)),
 1624    read(In, Result).
 1625read_reply(ContentType, In, _Result) :-
 1626    read_string(In, 500, String),
 1627    print_message(error, pack(no_prolog_response(ContentType, String))),
 1628    fail.
 1629
 1630info_level(Level, Options) :-
 1631    option(silent(true), Options),
 1632    !,
 1633    Level = silent.
 1634info_level(informational, _).
 1635
 1636message_severity(true(_), Informational, Informational).
 1637message_severity(false, warning, _).
 1638message_severity(exception(_), error, _).
 1639
 1640
 1641%!  inquiry_result(+Reply, +File, +Options) is semidet.
 1642%
 1643%   Analyse the results  of  the  inquiry   and  decide  whether  to
 1644%   continue or not.
 1645
 1646inquiry_result(Reply, File, Options) :-
 1647    findall(Eval, eval_inquiry(Reply, File, Eval, Options), Evaluation),
 1648    \+ member(cancel, Evaluation),
 1649    select_option(git(_), Options, Options1, _),
 1650    forall(member(install_dependencies(Resolution), Evaluation),
 1651           maplist(install_dependency(Options1), Resolution)).
 1652
 1653eval_inquiry(true(Reply), URL, Eval, _) :-
 1654    include(alt_hash, Reply, Alts),
 1655    Alts \== [],
 1656    print_message(warning, pack(alt_hashes(URL, Alts))),
 1657    (   memberchk(downloads(Count), Reply),
 1658        (   git_url(URL, _)
 1659        ->  Default = yes,
 1660            Eval = with_git_commits_in_same_version
 1661        ;   Default = no,
 1662            Eval = with_alt_hashes
 1663        ),
 1664        confirm(continue_with_alt_hashes(Count, URL), Default, [])
 1665    ->  true
 1666    ;   !,                          % Stop other rules
 1667        Eval = cancel
 1668    ).
 1669eval_inquiry(true(Reply), _, Eval, Options) :-
 1670    include(dependency, Reply, Deps),
 1671    Deps \== [],
 1672    select_dependency_resolution(Deps, Eval, Options),
 1673    (   Eval == cancel
 1674    ->  !
 1675    ;   true
 1676    ).
 1677eval_inquiry(true(Reply), URL, true, Options) :-
 1678    file_base_name(URL, File),
 1679    info_level(Informational, Options),
 1680    print_message(Informational, pack(inquiry_ok(Reply, File))).
 1681eval_inquiry(exception(pack(modified_hash(_SHA1-URL, _SHA2-[URL]))),
 1682             URL, Eval, Options) :-
 1683    (   confirm(continue_with_modified_hash(URL), no, Options)
 1684    ->  Eval = true
 1685    ;   Eval = cancel
 1686    ).
 1687
 1688alt_hash(alt_hash(_,_,_)).
 1689dependency(dependency(_,_,_,_,_)).
 1690
 1691
 1692%!  select_dependency_resolution(+Deps, -Eval, +Options)
 1693%
 1694%   Select a resolution.
 1695%
 1696%   @tbd    Exploit backtracking over resolve_dependencies/2.
 1697
 1698select_dependency_resolution(Deps, Eval, Options) :-
 1699    resolve_dependencies(Deps, Resolution),
 1700    exclude(local_dep, Resolution, ToBeDone),
 1701    (   ToBeDone == []
 1702    ->  !, Eval = true
 1703    ;   print_message(warning, pack(install_dependencies(Resolution))),
 1704        (   memberchk(_-unresolved, Resolution)
 1705        ->  Default = cancel
 1706        ;   Default = install_deps
 1707        ),
 1708        menu(pack(resolve_deps),
 1709             [ install_deps    = install_deps,
 1710               install_no_deps = install_no_deps,
 1711               cancel          = cancel
 1712             ], Default, Choice, Options),
 1713        (   Choice == cancel
 1714        ->  !, Eval = cancel
 1715        ;   Choice == install_no_deps
 1716        ->  !, Eval = install_no_deps
 1717        ;   !, Eval = install_dependencies(Resolution)
 1718        )
 1719    ).
 1720
 1721local_dep(_-resolved(_)).
 1722
 1723
 1724%!  install_dependency(+Options, +TokenResolution)
 1725%
 1726%   Install dependencies for the given resolution.
 1727%
 1728%   @tbd: Query URI to use
 1729
 1730install_dependency(Options,
 1731                   _Token-resolve(Pack, VersionAtom, [_URL|_], SubResolve)) :-
 1732    atom_version(VersionAtom, Version),
 1733    current_pack(Pack),
 1734    pack_info(Pack, _, version(InstalledAtom)),
 1735    atom_version(InstalledAtom, Installed),
 1736    Installed == Version,               % already installed
 1737    !,
 1738    maplist(install_dependency(Options), SubResolve).
 1739install_dependency(Options,
 1740                   _Token-resolve(Pack, VersionAtom, [URL|_], SubResolve)) :-
 1741    !,
 1742    atom_version(VersionAtom, Version),
 1743    merge_options([ url(URL),
 1744                    version(Version),
 1745                    interactive(false),
 1746                    inquiry(false),
 1747                    info(list),
 1748                    pack(Pack)
 1749                  ], Options, InstallOptions),
 1750    pack_install(Pack, InstallOptions),
 1751    maplist(install_dependency(Options), SubResolve).
 1752install_dependency(_, _-_).
 1753
 1754
 1755                 /*******************************
 1756                 *        WILDCARD URIs         *
 1757                 *******************************/
 1758
 1759%!  available_download_versions(+URL, -Versions) is det.
 1760%
 1761%   Deal with wildcard URLs, returning a  list of Version-URL pairs,
 1762%   sorted by version.
 1763%
 1764%   @tbd    Deal with protocols other than HTTP
 1765
 1766available_download_versions(URL, Versions) :-
 1767    wildcard_pattern(URL),
 1768    github_url(URL, User, Repo),
 1769    !,
 1770    findall(Version-VersionURL,
 1771            github_version(User, Repo, Version, VersionURL),
 1772            Versions).
 1773available_download_versions(URL, Versions) :-
 1774    wildcard_pattern(URL),
 1775    !,
 1776    file_directory_name(URL, DirURL0),
 1777    ensure_slash(DirURL0, DirURL),
 1778    print_message(informational, pack(query_versions(DirURL))),
 1779    setup_call_cleanup(
 1780        http_open(DirURL, In, []),
 1781        load_html(stream(In), DOM,
 1782                  [ syntax_errors(quiet)
 1783                  ]),
 1784        close(In)),
 1785    findall(MatchingURL,
 1786            absolute_matching_href(DOM, URL, MatchingURL),
 1787            MatchingURLs),
 1788    (   MatchingURLs == []
 1789    ->  print_message(warning, pack(no_matching_urls(URL)))
 1790    ;   true
 1791    ),
 1792    versioned_urls(MatchingURLs, VersionedURLs),
 1793    keysort(VersionedURLs, SortedVersions),
 1794    reverse(SortedVersions, Versions),
 1795    print_message(informational, pack(found_versions(Versions))).
 1796available_download_versions(URL, [Version-URL]) :-
 1797    (   pack_version_file(_Pack, Version0, URL)
 1798    ->  Version = Version0
 1799    ;   Version = unknown
 1800    ).
 1801
 1802%!  github_url(+URL, -User, -Repo) is semidet.
 1803%
 1804%   True when URL refers to a github repository.
 1805
 1806github_url(URL, User, Repo) :-
 1807    uri_components(URL, uri_components(https,'github.com',Path,_,_)),
 1808    atomic_list_concat(['',User,Repo|_], /, Path).
 1809
 1810
 1811%!  github_version(+User, +Repo, -Version, -VersionURI) is nondet.
 1812%
 1813%   True when Version is a release version and VersionURI is the
 1814%   download location for the zip file.
 1815
 1816github_version(User, Repo, Version, VersionURI) :-
 1817    atomic_list_concat(['',repos,User,Repo,tags], /, Path1),
 1818    uri_components(ApiUri, uri_components(https,'api.github.com',Path1,_,_)),
 1819    setup_call_cleanup(
 1820      http_open(ApiUri, In,
 1821                [ request_header('Accept'='application/vnd.github.v3+json')
 1822                ]),
 1823      json_read_dict(In, Dicts),
 1824      close(In)),
 1825    member(Dict, Dicts),
 1826    atom_string(Tag, Dict.name),
 1827    tag_version(Tag, Version),
 1828    atom_string(VersionURI, Dict.zipball_url).
 1829
 1830wildcard_pattern(URL) :- sub_atom(URL, _, _, _, *).
 1831wildcard_pattern(URL) :- sub_atom(URL, _, _, _, ?).
 1832
 1833ensure_slash(Dir, DirS) :-
 1834    (   sub_atom(Dir, _, _, 0, /)
 1835    ->  DirS = Dir
 1836    ;   atom_concat(Dir, /, DirS)
 1837    ).
 1838
 1839absolute_matching_href(DOM, Pattern, Match) :-
 1840    xpath(DOM, //a(@href), HREF),
 1841    uri_normalized(HREF, Pattern, Match),
 1842    wildcard_match(Pattern, Match).
 1843
 1844versioned_urls([], []).
 1845versioned_urls([H|T0], List) :-
 1846    file_base_name(H, File),
 1847    (   pack_version_file(_Pack, Version, File)
 1848    ->  List = [Version-H|T]
 1849    ;   List = T
 1850    ),
 1851    versioned_urls(T0, T).
 1852
 1853
 1854                 /*******************************
 1855                 *          DEPENDENCIES        *
 1856                 *******************************/
 1857
 1858%!  update_dependency_db
 1859%
 1860%   Reload dependency declarations between packages.
 1861
 1862update_dependency_db :-
 1863    retractall(pack_requires(_,_)),
 1864    retractall(pack_provides_db(_,_)),
 1865    forall(current_pack(Pack),
 1866           (   findall(Info, pack_info(Pack, dependency, Info), Infos),
 1867               update_dependency_db(Pack, Infos)
 1868           )).
 1869
 1870update_dependency_db(Name, Info) :-
 1871    retractall(pack_requires(Name, _)),
 1872    retractall(pack_provides_db(Name, _)),
 1873    maplist(assert_dep(Name), Info).
 1874
 1875assert_dep(Pack, provides(Token)) :-
 1876    !,
 1877    assertz(pack_provides_db(Pack, Token)).
 1878assert_dep(Pack, requires(Token)) :-
 1879    !,
 1880    assertz(pack_requires(Pack, Token)).
 1881assert_dep(_, _).
 1882
 1883%!  validate_dependencies is det.
 1884%
 1885%   Validate all dependencies, reporting on failures
 1886
 1887validate_dependencies :-
 1888    unsatisfied_dependencies(Unsatisfied),
 1889    !,
 1890    print_message(warning, pack(unsatisfied(Unsatisfied))).
 1891validate_dependencies.
 1892
 1893
 1894unsatisfied_dependencies(Unsatisfied) :-
 1895    findall(Req-Pack, pack_requires(Pack, Req), Reqs0),
 1896    keysort(Reqs0, Reqs1),
 1897    group_pairs_by_key(Reqs1, GroupedReqs),
 1898    exclude(satisfied_dependency, GroupedReqs, Unsatisfied),
 1899    Unsatisfied \== [].
 1900
 1901satisfied_dependency(Needed-_By) :-
 1902    pack_provides(_, Needed),
 1903    !.
 1904satisfied_dependency(Needed-_By) :-
 1905    compound(Needed),
 1906    Needed =.. [Op, Pack, ReqVersion],
 1907    (   pack_provides(Pack, Pack)
 1908    ->  pack_info(Pack, _, version(PackVersion)),
 1909        version_data(PackVersion, PackData)
 1910    ;   Pack == prolog
 1911    ->  current_prolog_flag(version_data, swi(Major,Minor,Patch,_)),
 1912        PackData = [Major,Minor,Patch]
 1913    ),
 1914    version_data(ReqVersion, ReqData),
 1915    cmp(Op, Cmp),
 1916    call(Cmp, PackData, ReqData).
 1917
 1918%!  pack_provides(?Package, ?Token) is multi.
 1919%
 1920%   True if Pack provides Token.  A package always provides itself.
 1921
 1922pack_provides(Pack, Pack) :-
 1923    current_pack(Pack).
 1924pack_provides(Pack, Token) :-
 1925    pack_provides_db(Pack, Token).
 1926
 1927%!  pack_depends_on(?Pack, ?Dependency) is nondet.
 1928%
 1929%   True if Pack requires Dependency, direct or indirect.
 1930
 1931pack_depends_on(Pack, Dependency) :-
 1932    (   atom(Pack)
 1933    ->  pack_depends_on_fwd(Pack, Dependency, [Pack])
 1934    ;   pack_depends_on_bwd(Pack, Dependency, [Dependency])
 1935    ).
 1936
 1937pack_depends_on_fwd(Pack, Dependency, Visited) :-
 1938    pack_depends_on_1(Pack, Dep1),
 1939    \+ memberchk(Dep1, Visited),
 1940    (   Dependency = Dep1
 1941    ;   pack_depends_on_fwd(Dep1, Dependency, [Dep1|Visited])
 1942    ).
 1943
 1944pack_depends_on_bwd(Pack, Dependency, Visited) :-
 1945    pack_depends_on_1(Dep1, Dependency),
 1946    \+ memberchk(Dep1, Visited),
 1947    (   Pack = Dep1
 1948    ;   pack_depends_on_bwd(Pack, Dep1, [Dep1|Visited])
 1949    ).
 1950
 1951pack_depends_on_1(Pack, Dependency) :-
 1952    atom(Dependency),
 1953    !,
 1954    pack_provides(Dependency, Token),
 1955    pack_requires(Pack, Token).
 1956pack_depends_on_1(Pack, Dependency) :-
 1957    pack_requires(Pack, Token),
 1958    pack_provides(Dependency, Token).
 1959
 1960
 1961%!  resolve_dependencies(+Dependencies, -Resolution) is multi.
 1962%
 1963%   Resolve dependencies as reported by the remote package server.
 1964%
 1965%   @param  Dependencies is a list of
 1966%           dependency(Token, Pack, Version, URLs, SubDeps)
 1967%   @param  Resolution is a list of items
 1968%           - Token-resolved(Pack)
 1969%           - Token-resolve(Pack, Version, URLs, SubResolve)
 1970%           - Token-unresolved
 1971%   @tbd    Watch out for conflicts
 1972%   @tbd    If there are different packs that resolve a token,
 1973%           make an intelligent choice instead of using the first
 1974
 1975resolve_dependencies(Dependencies, Resolution) :-
 1976    maplist(dependency_pair, Dependencies, Pairs0),
 1977    keysort(Pairs0, Pairs1),
 1978    group_pairs_by_key(Pairs1, ByToken),
 1979    maplist(resolve_dep, ByToken, Resolution).
 1980
 1981dependency_pair(dependency(Token, Pack, Version, URLs, SubDeps),
 1982                Token-(Pack-pack(Version,URLs, SubDeps))).
 1983
 1984resolve_dep(Token-Pairs, Token-Resolution) :-
 1985    (   resolve_dep2(Token-Pairs, Resolution)
 1986    *-> true
 1987    ;   Resolution = unresolved
 1988    ).
 1989
 1990resolve_dep2(Token-_, resolved(Pack)) :-
 1991    pack_provides(Pack, Token).
 1992resolve_dep2(_-Pairs, resolve(Pack, VersionAtom, URLs, SubResolves)) :-
 1993    keysort(Pairs, Sorted),
 1994    group_pairs_by_key(Sorted, ByPack),
 1995    member(Pack-Versions, ByPack),
 1996    Pack \== (-),
 1997    maplist(version_pack, Versions, VersionData),
 1998    sort(VersionData, ByVersion),
 1999    reverse(ByVersion, ByVersionLatest),
 2000    member(pack(Version,URLs,SubDeps), ByVersionLatest),
 2001    atom_version(VersionAtom, Version),
 2002    include(dependency, SubDeps, Deps),
 2003    resolve_dependencies(Deps, SubResolves).
 2004
 2005version_pack(pack(VersionAtom,URLs,SubDeps),
 2006             pack(Version,URLs,SubDeps)) :-
 2007    atom_version(VersionAtom, Version).
 2008
 2009
 2010
 2011%!  pack_attach(+Dir, +Options) is det.
 2012%
 2013%   Attach a single package in Dir.  The Dir is expected to contain
 2014%   the file `pack.pl` and a `prolog` directory.  Options processed:
 2015%
 2016%     - duplicate(+Action)
 2017%     What to do if the same package is already installed in a different
 2018%     directory.  Action is one of
 2019%       - warning
 2020%       Warn and ignore the package
 2021%       - keep
 2022%       Silently ignore the package
 2023%       - replace
 2024%       Unregister the existing and insert the new package
 2025%     - search(+Where)
 2026%     Determines the order of searching package library directories.
 2027%     Default is `last`, alternative is `first`.
 2028%
 2029%   @see attach_packs/2 to attach multiple packs from a directory.
 2030
 2031pack_attach(Dir, Options) :-
 2032    '$pack_attach'(Dir, Options).
 2033
 2034
 2035                 /*******************************
 2036                 *        USER INTERACTION      *
 2037                 *******************************/
 2038
 2039:- multifile prolog:message//1. 2040
 2041%!  menu(Question, +Alternatives, +Default, -Selection, +Options)
 2042
 2043menu(_Question, _Alternatives, Default, Selection, Options) :-
 2044    option(interactive(false), Options),
 2045    !,
 2046    Selection = Default.
 2047menu(Question, Alternatives, Default, Selection, _) :-
 2048    length(Alternatives, N),
 2049    between(1, 5, _),
 2050       print_message(query, Question),
 2051       print_menu(Alternatives, Default, 1),
 2052       print_message(query, pack(menu(select))),
 2053       read_selection(N, Choice),
 2054    !,
 2055    (   Choice == default
 2056    ->  Selection = Default
 2057    ;   nth1(Choice, Alternatives, Selection=_)
 2058    ->  true
 2059    ).
 2060
 2061print_menu([], _, _).
 2062print_menu([Value=Label|T], Default, I) :-
 2063    (   Value == Default
 2064    ->  print_message(query, pack(menu(default_item(I, Label))))
 2065    ;   print_message(query, pack(menu(item(I, Label))))
 2066    ),
 2067    I2 is I + 1,
 2068    print_menu(T, Default, I2).
 2069
 2070read_selection(Max, Choice) :-
 2071    get_single_char(Code),
 2072    (   answered_default(Code)
 2073    ->  Choice = default
 2074    ;   code_type(Code, digit(Choice)),
 2075        between(1, Max, Choice)
 2076    ->  true
 2077    ;   print_message(warning, pack(menu(reply(1,Max)))),
 2078        fail
 2079    ).
 2080
 2081%!  confirm(+Question, +Default, +Options) is semidet.
 2082%
 2083%   Ask for confirmation.
 2084%
 2085%   @param Default is one of =yes=, =no= or =none=.
 2086
 2087confirm(_Question, Default, Options) :-
 2088    Default \== none,
 2089    option(interactive(false), Options, true),
 2090    !,
 2091    Default == yes.
 2092confirm(Question, Default, _) :-
 2093    between(1, 5, _),
 2094       print_message(query, pack(confirm(Question, Default))),
 2095       read_yes_no(YesNo, Default),
 2096    !,
 2097    format(user_error, '~N', []),
 2098    YesNo == yes.
 2099
 2100read_yes_no(YesNo, Default) :-
 2101    get_single_char(Code),
 2102    code_yes_no(Code, Default, YesNo),
 2103    !.
 2104
 2105code_yes_no(0'y, _, yes).
 2106code_yes_no(0'Y, _, yes).
 2107code_yes_no(0'n, _, no).
 2108code_yes_no(0'N, _, no).
 2109code_yes_no(_, none, _) :- !, fail.
 2110code_yes_no(C, Default, Default) :-
 2111    answered_default(C).
 2112
 2113answered_default(0'\r).
 2114answered_default(0'\n).
 2115answered_default(0'\s).
 2116
 2117
 2118                 /*******************************
 2119                 *            MESSAGES          *
 2120                 *******************************/
 2121
 2122:- multifile prolog:message//1. 2123
 2124prolog:message(pack(Message)) -->
 2125    message(Message).
 2126
 2127:- discontiguous
 2128    message//1,
 2129    label//1. 2130
 2131message(invalid_info(Term)) -->
 2132    [ 'Invalid package description: ~q'-[Term] ].
 2133message(directory_exists(Dir)) -->
 2134    [ 'Package target directory exists and is not empty:', nl,
 2135      '\t~q'-[Dir]
 2136    ].
 2137message(already_installed(pack(Pack, Version))) -->
 2138    { atom_version(AVersion, Version) },
 2139    [ 'Pack `~w'' is already installed @~w'-[Pack, AVersion] ].
 2140message(already_installed(Pack)) -->
 2141    [ 'Pack `~w'' is already installed. Package info:'-[Pack] ].
 2142message(invalid_name(File)) -->
 2143    [ '~w: A package archive must be named <pack>-<version>.<ext>'-[File] ],
 2144    no_tar_gz(File).
 2145
 2146no_tar_gz(File) -->
 2147    { sub_atom(File, _, _, 0, '.tar.gz') },
 2148    !,
 2149    [ nl,
 2150      'Package archive files must have a single extension.  E.g., \'.tgz\''-[]
 2151    ].
 2152no_tar_gz(_) --> [].
 2153
 2154message(kept_foreign(Pack)) -->
 2155    [ 'Found foreign libraries for target platform.'-[], nl,
 2156      'Use ?- pack_rebuild(~q). to rebuild from sources'-[Pack]
 2157    ].
 2158message(no_pack_installed(Pack)) -->
 2159    [ 'No pack ~q installed.  Use ?- pack_list(Pattern) to search'-[Pack] ].
 2160message(no_packages_installed) -->
 2161    { setting(server, ServerBase) },
 2162    [ 'There are no extra packages installed.', nl,
 2163      'Please visit ~wlist.'-[ServerBase]
 2164    ].
 2165message(remove_with(Pack)) -->
 2166    [ 'The package can be removed using: ?- ~q.'-[pack_remove(Pack)]
 2167    ].
 2168message(unsatisfied(Packs)) -->
 2169    [ 'The following dependencies are not satisfied:', nl ],
 2170    unsatisfied(Packs).
 2171message(depends(Pack, Deps)) -->
 2172    [ 'The following packages depend on `~w\':'-[Pack], nl ],
 2173    pack_list(Deps).
 2174message(remove(PackDir)) -->
 2175    [ 'Removing ~q and contents'-[PackDir] ].
 2176message(remove_existing_pack(PackDir)) -->
 2177    [ 'Remove old installation in ~q'-[PackDir] ].
 2178message(install_from(Pack, Version, git(URL))) -->
 2179    [ 'Install ~w@~w from GIT at ~w'-[Pack, Version, URL] ].
 2180message(install_from(Pack, Version, URL)) -->
 2181    [ 'Install ~w@~w from ~w'-[Pack, Version, URL] ].
 2182message(select_install_from(Pack, Version)) -->
 2183    [ 'Select download location for ~w@~w'-[Pack, Version] ].
 2184message(install_downloaded(File)) -->
 2185    { file_base_name(File, Base),
 2186      size_file(File, Size) },
 2187    [ 'Install "~w" (~D bytes)'-[Base, Size] ].
 2188message(git_post_install(PackDir, Pack)) -->
 2189    (   { is_foreign_pack(PackDir, _) }
 2190    ->  [ 'Run post installation scripts for pack "~w"'-[Pack] ]
 2191    ;   [ 'Activate pack "~w"'-[Pack] ]
 2192    ).
 2193message(no_meta_data(BaseDir)) -->
 2194    [ 'Cannot find pack.pl inside directory ~q.  Not a package?'-[BaseDir] ].
 2195message(inquiry(Server)) -->
 2196    [ 'Verify package status (anonymously)', nl,
 2197      '\tat "~w"'-[Server]
 2198    ].
 2199message(search_no_matches(Name)) -->
 2200    [ 'Search for "~w", returned no matching packages'-[Name] ].
 2201message(rebuild(Pack)) -->
 2202    [ 'Checking pack "~w" for rebuild ...'-[Pack] ].
 2203message(upgrade(Pack, From, To)) -->
 2204    [ 'Upgrade "~w" from '-[Pack] ],
 2205    msg_version(From), [' to '-[]], msg_version(To).
 2206message(up_to_date(Pack)) -->
 2207    [ 'Package "~w" is up-to-date'-[Pack] ].
 2208message(query_versions(URL)) -->
 2209    [ 'Querying "~w" to find new versions ...'-[URL] ].
 2210message(no_matching_urls(URL)) -->
 2211    [ 'Could not find any matching URL: ~q'-[URL] ].
 2212message(found_versions([Latest-_URL|More])) -->
 2213    { length(More, Len),
 2214      atom_version(VLatest, Latest)
 2215    },
 2216    [ '    Latest version: ~w (~D older)'-[VLatest, Len] ].
 2217message(process_output(Codes)) -->
 2218    { split_lines(Codes, Lines) },
 2219    process_lines(Lines).
 2220message(contacting_server(Server)) -->
 2221    [ 'Contacting server at ~w ...'-[Server], flush ].
 2222message(server_reply(true(_))) -->
 2223    [ at_same_line, ' ok'-[] ].
 2224message(server_reply(false)) -->
 2225    [ at_same_line, ' done'-[] ].
 2226message(server_reply(exception(E))) -->
 2227    [ 'Server reported the following error:'-[], nl ],
 2228    '$messages':translate_message(E).
 2229message(cannot_create_dir(Alias)) -->
 2230    { findall(PackDir,
 2231              absolute_file_name(Alias, PackDir, [solutions(all)]),
 2232              PackDirs0),
 2233      sort(PackDirs0, PackDirs)
 2234    },
 2235    [ 'Cannot find a place to create a package directory.'-[],
 2236      'Considered:'-[]
 2237    ],
 2238    candidate_dirs(PackDirs).
 2239message(no_match(Name)) -->
 2240    [ 'No registered pack matches "~w"'-[Name] ].
 2241message(conflict(version, [PackV, FileV])) -->
 2242    ['Version mismatch: pack.pl: '-[]], msg_version(PackV),
 2243    [', file claims version '-[]], msg_version(FileV).
 2244message(conflict(name, [PackInfo, FileInfo])) -->
 2245    ['Pack ~w mismatch: pack.pl: ~p'-[PackInfo]],
 2246    [', file claims ~w: ~p'-[FileInfo]].
 2247message(no_prolog_response(ContentType, String)) -->
 2248    [ 'Expected Prolog response.  Got content of type ~p'-[ContentType], nl,
 2249      '~s'-[String]
 2250    ].
 2251message(pack(no_upgrade_info(Pack))) -->
 2252    [ '~w: pack meta-data does not provide an upgradable URL'-[Pack] ].
 2253
 2254candidate_dirs([]) --> [].
 2255candidate_dirs([H|T]) --> [ nl, '    ~w'-[H] ], candidate_dirs(T).
 2256
 2257                                                % Questions
 2258message(resolve_remove) -->
 2259    [ nl, 'Please select an action:', nl, nl ].
 2260message(create_pack_dir) -->
 2261    [ nl, 'Create directory for packages', nl ].
 2262message(menu(item(I, Label))) -->
 2263    [ '~t(~d)~6|   '-[I] ],
 2264    label(Label).
 2265message(menu(default_item(I, Label))) -->
 2266    [ '~t(~d)~6| * '-[I] ],
 2267    label(Label).
 2268message(menu(select)) -->
 2269    [ nl, 'Your choice? ', flush ].
 2270message(confirm(Question, Default)) -->
 2271    message(Question),
 2272    confirm_default(Default),
 2273    [ flush ].
 2274message(menu(reply(Min,Max))) -->
 2275    (  { Max =:= Min+1 }
 2276    -> [ 'Please enter ~w or ~w'-[Min,Max] ]
 2277    ;  [ 'Please enter a number between ~w and ~w'-[Min,Max] ]
 2278    ).
 2279
 2280% Alternate hashes for found for the same file
 2281
 2282message(alt_hashes(URL, _Alts)) -->
 2283    { git_url(URL, _)
 2284    },
 2285    !,
 2286    [ 'GIT repository was updated without updating version' ].
 2287message(alt_hashes(URL, Alts)) -->
 2288    { file_base_name(URL, File)
 2289    },
 2290    [ 'Found multiple versions of "~w".'-[File], nl,
 2291      'This could indicate a compromised or corrupted file', nl
 2292    ],
 2293    alt_hashes(Alts).
 2294message(continue_with_alt_hashes(Count, URL)) -->
 2295    [ 'Continue installation from "~w" (downloaded ~D times)'-[URL, Count] ].
 2296message(continue_with_modified_hash(_URL)) -->
 2297    [ 'Pack may be compromised.  Continue anyway'
 2298    ].
 2299message(modified_hash(_SHA1-URL, _SHA2-[URL])) -->
 2300    [ 'Content of ~q has changed.'-[URL]
 2301    ].
 2302
 2303alt_hashes([]) --> [].
 2304alt_hashes([H|T]) --> alt_hash(H), ( {T == []} -> [] ; [nl], alt_hashes(T) ).
 2305
 2306alt_hash(alt_hash(Count, URLs, Hash)) -->
 2307    [ '~t~d~8| ~w'-[Count, Hash] ],
 2308    alt_urls(URLs).
 2309
 2310alt_urls([]) --> [].
 2311alt_urls([H|T]) -->
 2312    [ nl, '    ~w'-[H] ],
 2313    alt_urls(T).
 2314
 2315% Installation dependencies gathered from inquiry server.
 2316
 2317message(install_dependencies(Resolution)) -->
 2318    [ 'Package depends on the following:' ],
 2319    msg_res_tokens(Resolution, 1).
 2320
 2321msg_res_tokens([], _) --> [].
 2322msg_res_tokens([H|T], L) --> msg_res_token(H, L), msg_res_tokens(T, L).
 2323
 2324msg_res_token(Token-unresolved, L) -->
 2325    res_indent(L),
 2326    [ '"~w" cannot be satisfied'-[Token] ].
 2327msg_res_token(Token-resolve(Pack, Version, [URL|_], SubResolves), L) -->
 2328    !,
 2329    res_indent(L),
 2330    [ '"~w", provided by ~w@~w from ~w'-[Token, Pack, Version, URL] ],
 2331    { L2 is L+1 },
 2332    msg_res_tokens(SubResolves, L2).
 2333msg_res_token(Token-resolved(Pack), L) -->
 2334    !,
 2335    res_indent(L),
 2336    [ '"~w", provided by installed pack ~w'-[Token,Pack] ].
 2337
 2338res_indent(L) -->
 2339    { I is L*2 },
 2340    [ nl, '~*c'-[I,0'\s] ].
 2341
 2342message(resolve_deps) -->
 2343    [ nl, 'What do you wish to do' ].
 2344label(install_deps) -->
 2345    [ 'Install proposed dependencies' ].
 2346label(install_no_deps) -->
 2347    [ 'Only install requested package' ].
 2348
 2349
 2350message(git_fetch(Dir)) -->
 2351    [ 'Running "git fetch" in ~q'-[Dir] ].
 2352
 2353% inquiry is blank
 2354
 2355message(inquiry_ok(Reply, File)) -->
 2356    { memberchk(downloads(Count), Reply),
 2357      memberchk(rating(VoteCount, Rating), Reply),
 2358      !,
 2359      length(Stars, Rating),
 2360      maplist(=(0'*), Stars)
 2361    },
 2362    [ '"~w" was downloaded ~D times.  Package rated ~s (~D votes)'-
 2363      [ File, Count, Stars, VoteCount ]
 2364    ].
 2365message(inquiry_ok(Reply, File)) -->
 2366    { memberchk(downloads(Count), Reply)
 2367    },
 2368    [ '"~w" was downloaded ~D times'-[ File, Count ] ].
 2369
 2370                                                % support predicates
 2371unsatisfied([]) --> [].
 2372unsatisfied([Needed-[By]|T]) -->
 2373    [ '  - "~w" is needed by package "~w"'-[Needed, By], nl ],
 2374    unsatisfied(T).
 2375unsatisfied([Needed-By|T]) -->
 2376    [ '  - "~w" is needed by the following packages:'-[Needed], nl ],
 2377    pack_list(By),
 2378    unsatisfied(T).
 2379
 2380pack_list([]) --> [].
 2381pack_list([H|T]) -->
 2382    [ '    - Package "~w"'-[H], nl ],
 2383    pack_list(T).
 2384
 2385process_lines([]) --> [].
 2386process_lines([H|T]) -->
 2387    [ '~s'-[H] ],
 2388    (   {T==[]}
 2389    ->  []
 2390    ;   [nl], process_lines(T)
 2391    ).
 2392
 2393split_lines([], []) :- !.
 2394split_lines(All, [Line1|More]) :-
 2395    append(Line1, [0'\n|Rest], All),
 2396    !,
 2397    split_lines(Rest, More).
 2398split_lines(Line, [Line]).
 2399
 2400label(remove_only(Pack)) -->
 2401    [ 'Only remove package ~w (break dependencies)'-[Pack] ].
 2402label(remove_deps(Pack, Deps)) -->
 2403    { length(Deps, Count) },
 2404    [ 'Remove package ~w and ~D dependencies'-[Pack, Count] ].
 2405label(create_dir(Dir)) -->
 2406    [ '~w'-[Dir] ].
 2407label(install_from(git(URL))) -->
 2408    !,
 2409    [ 'GIT repository at ~w'-[URL] ].
 2410label(install_from(URL)) -->
 2411    [ '~w'-[URL] ].
 2412label(cancel) -->
 2413    [ 'Cancel' ].
 2414
 2415confirm_default(yes) -->
 2416    [ ' Y/n? ' ].
 2417confirm_default(no) -->
 2418    [ ' y/N? ' ].
 2419confirm_default(none) -->
 2420    [ ' y/n? ' ].
 2421
 2422msg_version(Version) -->
 2423    { atom(Version) },
 2424    !,
 2425    [ '~w'-[Version] ].
 2426msg_version(VersionData) -->
 2427    !,
 2428    { atom_version(Atom, VersionData) },
 2429    [ '~w'-[Atom] ]