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