View source with formatted comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        jan@swi-prolog.org
    5    WWW:           https://www.swi-prolog.org
    6    Copyright (c)  2012-2024, 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_list/2,                % +Query, +Options
   42            pack_search/1,              % +Keyword
   43            pack_install/1,             % +Name
   44            pack_install/2,             % +Name, +Options
   45            pack_install_local/3,       % :Spec, +Dir, +Options
   46            pack_upgrade/1,             % +Name
   47            pack_rebuild/1,             % +Name
   48            pack_rebuild/0,             % All packages
   49            pack_remove/1,              % +Name
   50            pack_remove/2,              % +Name, +Options
   51            pack_publish/2,             % +URL, +Options
   52            pack_property/2             % ?Name, ?Property
   53          ]).   54:- use_module(library(apply)).   55:- use_module(library(error)).   56:- use_module(library(option)).   57:- use_module(library(readutil)).   58:- use_module(library(lists)).   59:- use_module(library(filesex)).   60:- use_module(library(xpath)).   61:- use_module(library(settings)).   62:- use_module(library(uri)).   63:- use_module(library(dcg/basics)).   64:- use_module(library(dcg/high_order)).   65:- use_module(library(http/http_open)).   66:- use_module(library(json)).   67:- use_module(library(http/http_client), []).   68:- use_module(library(debug), [assertion/1]).   69:- use_module(library(pairs),
   70              [pairs_keys/2, map_list_to_pairs/3, pairs_values/2]).   71:- autoload(library(git)).   72:- autoload(library(sgml)).   73:- autoload(library(sha)).   74:- autoload(library(build/tools)).   75:- autoload(library(ansi_term), [ansi_format/3]).   76:- autoload(library(pprint), [print_term/2]).   77:- autoload(library(prolog_versions), [require_version/3, cmp_versions/3]).   78:- autoload(library(ugraphs), [vertices_edges_to_ugraph/3, ugraph_layers/2]).   79:- autoload(library(process), [process_which/2]).   80:- autoload(library(aggregate), [aggregate_all/3]).   81
   82:- meta_predicate
   83    pack_install_local(2, +, +).   84
   85/** <module> A package manager for Prolog
   86
   87The library(prolog_pack) provides the SWI-Prolog   package manager. This
   88library lets you inspect installed   packages,  install packages, remove
   89packages, etc. This library complemented by the built-in predicates such
   90as attach_packs/2 that makes installed packages available as libraries.
   91
   92The important functionality of this library is encapsulated in the _app_
   93`pack`. For help, run
   94
   95    swipl pack help
   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		 *       LOCAL DECLARATIONS	*
  108		 *******************************/
  109
  110:- op(900, xfx, @).                     % Token@Version
  111
  112:- meta_predicate det_if(0,0).  113
  114                 /*******************************
  115                 *         PACKAGE INFO         *
  116                 *******************************/
  117
  118%!  current_pack(?Pack) is nondet.
  119%!  current_pack(?Pack, ?Dir) is nondet.
  120%
  121%   True if Pack is a currently installed pack.
  122
  123current_pack(Pack) :-
  124    current_pack(Pack, _).
  125
  126current_pack(Pack, Dir) :-
  127    '$pack':pack(Pack, Dir).
  128
  129%!  pack_list_installed is det.
  130%
  131%   List currently installed packages  and   report  possible dependency
  132%   issues.
  133
  134pack_list_installed :-
  135    pack_list('', [installed(true)]),
  136    validate_dependencies.
  137
  138%!  pack_info(+Pack)
  139%
  140%   Print more detailed information about Pack.
  141
  142pack_info(Name) :-
  143    pack_info(info, Name).
  144
  145pack_info(Level, Name) :-
  146    must_be(atom, Name),
  147    findall(Info, pack_info(Name, Level, Info), Infos0),
  148    (   Infos0 == []
  149    ->  print_message(warning, pack(no_pack_installed(Name))),
  150        fail
  151    ;   true
  152    ),
  153    findall(Def,  pack_default(Level, Infos, Def), Defs),
  154    append(Infos0, Defs, Infos1),
  155    sort(Infos1, Infos),
  156    show_info(Name, Infos, [info(Level)]).
  157
  158
  159show_info(_Name, _Properties, Options) :-
  160    option(silent(true), Options),
  161    !.
  162show_info(_Name, _Properties, Options) :-
  163    option(show_info(false), Options),
  164    !.
  165show_info(Name, Properties, Options) :-
  166    option(info(list), Options),
  167    !,
  168    memberchk(title(Title), Properties),
  169    memberchk(version(Version), Properties),
  170    format('i ~w@~w ~28|- ~w~n', [Name, Version, Title]).
  171show_info(Name, Properties, _) :-
  172    !,
  173    print_property_value('Package'-'~w', [Name]),
  174    findall(Term, pack_level_info(info, Term, _, _), Terms),
  175    maplist(print_property(Properties), Terms).
  176
  177print_property(_, nl) :-
  178    !,
  179    format('~n').
  180print_property(Properties, Term) :-
  181    findall(Term, member(Term, Properties), Terms),
  182    Terms \== [],
  183    !,
  184    pack_level_info(_, Term, LabelFmt, _Def),
  185    (   LabelFmt = Label-FmtElem
  186    ->  true
  187    ;   Label = LabelFmt,
  188        FmtElem = '~w'
  189    ),
  190    multi_valued(Terms, FmtElem, FmtList, Values),
  191    atomic_list_concat(FmtList, ', ', Fmt),
  192    print_property_value(Label-Fmt, Values).
  193print_property(_, _).
  194
  195multi_valued([H], LabelFmt, [LabelFmt], Values) :-
  196    !,
  197    H =.. [_|Values].
  198multi_valued([H|T], LabelFmt, [LabelFmt|LT], Values) :-
  199    H =.. [_|VH],
  200    append(VH, MoreValues, Values),
  201    multi_valued(T, LabelFmt, LT, MoreValues).
  202
  203
  204pvalue_column(31).
  205print_property_value(Prop-Fmt, Values) :-
  206    !,
  207    pvalue_column(C),
  208    ansi_format(comment, '% ~w:~t~*|', [Prop, C]),
  209    ansi_format(code, Fmt, Values),
  210    ansi_format([], '~n', []).
  211
  212pack_info(Name, Level, Info) :-
  213    '$pack':pack(Name, BaseDir),
  214    pack_dir_info(BaseDir, Level, Info).
  215
  216pack_dir_info(BaseDir, Level, Info) :-
  217    (   Info = directory(BaseDir)
  218    ;   pack_info_term(BaseDir, Info)
  219    ),
  220    pack_level_info(Level, Info, _Format, _Default).
  221
  222:- public pack_level_info/4.                    % used by web-server
  223
  224pack_level_info(_,    title(_),         'Title',                   '<no title>').
  225pack_level_info(_,    version(_),       'Installed version',       '<unknown>').
  226pack_level_info(info, automatic(_),	'Automatic (dependency only)', -).
  227pack_level_info(info, directory(_),     'Installed in directory',  -).
  228pack_level_info(info, link(_),		'Installed as link to'-'~w', -).
  229pack_level_info(info, built(_,_),	'Built on'-'~w for SWI-Prolog ~w', -).
  230pack_level_info(info, author(_, _),     'Author'-'~w <~w>',        -).
  231pack_level_info(info, maintainer(_, _), 'Maintainer'-'~w <~w>',    -).
  232pack_level_info(info, packager(_, _),   'Packager'-'~w <~w>',      -).
  233pack_level_info(info, home(_),          'Home page',               -).
  234pack_level_info(info, download(_),      'Download URL',            -).
  235pack_level_info(_,    provides(_),      'Provides',                -).
  236pack_level_info(_,    requires(_),      'Requires',                -).
  237pack_level_info(_,    conflicts(_),     'Conflicts with',          -).
  238pack_level_info(_,    replaces(_),      'Replaces packages',       -).
  239pack_level_info(info, library(_),	'Provided libraries',      -).
  240pack_level_info(info, autoload(_),	'Autoload',                -).
  241
  242pack_default(Level, Infos, Def) :-
  243    pack_level_info(Level, ITerm, _Format, Def),
  244    Def \== (-),
  245    \+ memberchk(ITerm, Infos).
  246
  247%!  pack_info_term(+PackDir, ?Info) is nondet.
  248%
  249%   True when Info is meta-data for the package PackName.
  250
  251pack_info_term(BaseDir, Info) :-
  252    directory_file_path(BaseDir, 'pack.pl', InfoFile),
  253    catch(
  254        term_in_file(valid_term(pack_info_term), InfoFile, Info),
  255        error(existence_error(source_sink, InfoFile), _),
  256        ( print_message(error, pack(no_meta_data(BaseDir))),
  257          fail
  258        )).
  259pack_info_term(BaseDir, library(Lib)) :-
  260    atom_concat(BaseDir, '/prolog/', LibDir),
  261    atom_concat(LibDir, '*.pl', Pattern),
  262    expand_file_name(Pattern, Files),
  263    maplist(atom_concat(LibDir), Plain, Files),
  264    convlist(base_name, Plain, Libs),
  265    member(Lib, Libs),
  266    Lib \== 'INDEX'.
  267pack_info_term(BaseDir, autoload(true)) :-
  268    atom_concat(BaseDir, '/prolog/INDEX.pl', IndexFile),
  269    exists_file(IndexFile).
  270pack_info_term(BaseDir, automatic(Boolean)) :-
  271    once(pack_status_dir(BaseDir, automatic(Boolean))).
  272pack_info_term(BaseDir, built(Arch, Prolog)) :-
  273    pack_status_dir(BaseDir, built(Arch, Prolog, _How)).
  274pack_info_term(BaseDir, link(Dest)) :-
  275    read_link(BaseDir, _, Dest).
  276
  277base_name(File, Base) :-
  278    file_name_extension(Base, pl, File).
  279
  280%!  term_in_file(:Valid, +File, -Term) is nondet.
  281%
  282%   True when Term appears in file and call(Valid, Term) is true.
  283
  284:- meta_predicate
  285    term_in_file(1, +, -).  286
  287term_in_file(Valid, File, Term) :-
  288    exists_file(File),
  289    setup_call_cleanup(
  290        open(File, read, In, [encoding(utf8)]),
  291        term_in_stream(Valid, In, Term),
  292        close(In)).
  293
  294term_in_stream(Valid, In, Term) :-
  295    repeat,
  296        read_term(In, Term0, []),
  297        (   Term0 == end_of_file
  298        ->  !, fail
  299        ;   Term = Term0,
  300            call(Valid, Term0)
  301        ).
  302
  303:- meta_predicate
  304    valid_term(1,+).  305
  306valid_term(Type, Term) :-
  307    Term =.. [Name|Args],
  308    same_length(Args, Types),
  309    Decl =.. [Name|Types],
  310    (   call(Type, Decl)
  311    ->  maplist(valid_info_arg, Types, Args)
  312    ;   print_message(warning, pack(invalid_term(Type, Term))),
  313        fail
  314    ).
  315
  316valid_info_arg(Type, Arg) :-
  317    must_be(Type, Arg).
  318
  319%!  pack_info_term(?Term) is nondet.
  320%
  321%   True when Term describes name and   arguments of a valid package
  322%   info term.
  323
  324pack_info_term(name(atom)).                     % Synopsis
  325pack_info_term(title(atom)).
  326pack_info_term(keywords(list(atom))).
  327pack_info_term(description(list(atom))).
  328pack_info_term(version(version)).
  329pack_info_term(author(atom, email_or_url_or_empty)).     % Persons
  330pack_info_term(maintainer(atom, email_or_url)).
  331pack_info_term(packager(atom, email_or_url)).
  332pack_info_term(pack_version(nonneg)).           % Package convention version
  333pack_info_term(home(atom)).                     % Home page
  334pack_info_term(download(atom)).                 % Source
  335pack_info_term(provides(atom)).                 % Dependencies
  336pack_info_term(requires(dependency)).
  337pack_info_term(conflicts(dependency)).          % Conflicts with package
  338pack_info_term(replaces(atom)).                 % Replaces another package
  339pack_info_term(autoload(boolean)).              % Default installation options
  340
  341:- multifile
  342    error:has_type/2.  343
  344error:has_type(version, Version) :-
  345    atom(Version),
  346    is_version(Version).
  347error:has_type(email_or_url, Address) :-
  348    atom(Address),
  349    (   sub_atom(Address, _, _, _, @)
  350    ->  true
  351    ;   uri_is_global(Address)
  352    ).
  353error:has_type(email_or_url_or_empty, Address) :-
  354    (   Address == ''
  355    ->  true
  356    ;   error:has_type(email_or_url, Address)
  357    ).
  358error:has_type(dependency, Value) :-
  359    is_dependency(Value).
  360
  361is_version(Version) :-
  362    split_string(Version, ".", "", Parts),
  363    maplist(number_string, _, Parts).
  364
  365is_dependency(Var) :-
  366    var(Var),
  367    !,
  368    fail.
  369is_dependency(Token) :-
  370    atom(Token),
  371    !.
  372is_dependency(Term) :-
  373    compound(Term),
  374    compound_name_arguments(Term, Op, [Token,Version]),
  375    atom(Token),
  376    cmp(Op, _),
  377    is_version(Version),
  378    !.
  379is_dependency(PrologToken) :-
  380    is_prolog_token(PrologToken).
  381
  382cmp(<,  @<).
  383cmp(=<, @=<).
  384cmp(==, ==).
  385cmp(>=, @>=).
  386cmp(>,  @>).
  387
  388
  389                 /*******************************
  390                 *            SEARCH            *
  391                 *******************************/
  392
  393%!  pack_list(+Query) is det.
  394%!  pack_list(+Query, +Options) is det.
  395%!  pack_search(+Query) is det.
  396%
  397%   Query package server and  installed   packages  and display results.
  398%   Query is matches case-insensitively against the   name  and title of
  399%   known and installed packages. For each   matching  package, a single
  400%   line is displayed that provides:
  401%
  402%     - Installation status
  403%       - __p__: package, not installed
  404%       - __i__: installed package; up-to-date with public version
  405%       - __a__: as __i__, but installed only as dependency
  406%       - __U__: installed package; can be upgraded
  407%       - __A__: installed package; newer than publically available
  408%       - __l__: installed package; not on server
  409%     - Name@Version
  410%     - Name@Version(ServerVersion)
  411%     - Title
  412%
  413%   Options processed:
  414%
  415%     - installed(true)
  416%       Only list packages that are locally installed.  Contacts the
  417%       server to compare our local version to the latest available
  418%       version.
  419%     - outdated(true)
  420%       Only list packages that need to be updated.  This option
  421%       implies installed(true).
  422%     - server(Server|false)
  423%       If `false`, do not contact the server. This implies
  424%       installed(true).  Otherwise, use the given pack server.
  425%
  426%   Hint: ``?- pack_list('').`` lists all known packages.
  427%
  428%   The predicates pack_list/1 and  pack_search/1   are  synonyms.  Both
  429%   contact the package server  at   https://www.swi-prolog.org  to find
  430%   available packages. Contacting the server can   be avoided using the
  431%   server(false) option.
  432
  433pack_list(Query) :-
  434    pack_list(Query, []).
  435
  436pack_search(Query) :-
  437    pack_list(Query, []).
  438
  439pack_list(Query, Options) :-
  440    (   option(installed(true), Options)
  441    ;   option(outdated(true), Options)
  442    ;   option(server(false), Options)
  443    ),
  444    !,
  445    local_search(Query, Local),
  446    maplist(arg(1), Local, Packs),
  447    (   option(server(false), Options)
  448    ->  Hits = []
  449    ;   query_pack_server(info(Packs), true(Hits), Options)
  450    ),
  451    list_hits(Hits, Local, Options).
  452pack_list(Query, Options) :-
  453    query_pack_server(search(Query), Result, Options),
  454    (   Result == false
  455    ->  (   local_search(Query, Packs),
  456            Packs \== []
  457        ->  forall(member(pack(Pack, Stat, Title, Version, _), Packs),
  458                   format('~w ~w@~w ~28|- ~w~n',
  459                          [Stat, Pack, Version, Title]))
  460        ;   print_message(warning, pack(search_no_matches(Query)))
  461        )
  462    ;   Result = true(Hits), % Hits = list(pack(Name, p, Title, Version, URL))
  463        local_search(Query, Local),
  464        list_hits(Hits, Local, [])
  465    ).
  466
  467list_hits(Hits, Local, Options) :-
  468    append(Hits, Local, All),
  469    sort(All, Sorted),
  470    join_status(Sorted, Packs0),
  471    include(filtered(Options), Packs0, Packs),
  472    maplist(list_hit(Options), Packs).
  473
  474filtered(Options, pack(_,Tag,_,_,_)) :-
  475    option(outdated(true), Options),
  476    !,
  477    Tag == 'U'.
  478filtered(_, _).
  479
  480list_hit(_Options, pack(Pack, Tag, Title, Version, _URL)) =>
  481    list_tag(Tag),
  482    ansi_format(code, '~w', [Pack]),
  483    format('@'),
  484    list_version(Tag, Version),
  485    format('~35|- ', []),
  486    ansi_format(comment, '~w~n', [Title]).
  487
  488list_tag(Tag) :-
  489    tag_color(Tag, Color),
  490    ansi_format(Color, '~w ', [Tag]).
  491
  492list_version(Tag, VersionI-VersionS) =>
  493    tag_color(Tag, Color),
  494    ansi_format(Color, '~w', [VersionI]),
  495    ansi_format(bold, '(~w)', [VersionS]).
  496list_version(_Tag, Version) =>
  497    ansi_format([], '~w', [Version]).
  498
  499tag_color('U', warning) :- !.
  500tag_color('A', comment) :- !.
  501tag_color(_, []).
  502
  503%!  join_status(+PacksIn, -PacksOut) is det.
  504%
  505%   Combine local and remote information to   assess  the status of each
  506%   package. PacksOut is a list of  pack(Name, Status, Version, URL). If
  507%   the     versions     do      not       match,      `Version`      is
  508%   `VersionInstalled-VersionRemote` and similar for thee URL.
  509
  510join_status([], []).
  511join_status([ pack(Pack, i, Title, Version, URL),
  512              pack(Pack, p, Title, Version, _)
  513            | T0
  514            ],
  515            [ pack(Pack, Tag, Title, Version, URL)
  516            | T
  517            ]) :-
  518    !,
  519    (   pack_status(Pack, automatic(true))
  520    ->  Tag = a
  521    ;   Tag = i
  522    ),
  523    join_status(T0, T).
  524join_status([ pack(Pack, i, Title, VersionI, URLI),
  525              pack(Pack, p, _,     VersionS, URLS)
  526            | T0
  527            ],
  528            [ pack(Pack, Tag, Title, VersionI-VersionS, URLI-URLS)
  529            | T
  530            ]) :-
  531    !,
  532    version_sort_key(VersionI, VDI),
  533    version_sort_key(VersionS, VDS),
  534    (   VDI @< VDS
  535    ->  Tag = 'U'
  536    ;   Tag = 'A'
  537    ),
  538    join_status(T0, T).
  539join_status([ pack(Pack, i, Title, VersionI, URL)
  540            | T0
  541            ],
  542            [ pack(Pack, l, Title, VersionI, URL)
  543            | T
  544            ]) :-
  545    !,
  546    join_status(T0, T).
  547join_status([H|T0], [H|T]) :-
  548    join_status(T0, T).
  549
  550%!  local_search(+Query, -Packs:list(atom)) is det.
  551%
  552%   Search locally installed packs.
  553
  554local_search(Query, Packs) :-
  555    findall(Pack, matching_installed_pack(Query, Pack), Packs).
  556
  557matching_installed_pack(Query, pack(Pack, i, Title, Version, URL)) :-
  558    current_pack(Pack),
  559    findall(Term,
  560            ( pack_info(Pack, _, Term),
  561              search_info(Term)
  562            ), Info),
  563    (   sub_atom_icasechk(Pack, _, Query)
  564    ->  true
  565    ;   memberchk(title(Title), Info),
  566        sub_atom_icasechk(Title, _, Query)
  567    ),
  568    option(title(Title), Info, '<no title>'),
  569    option(version(Version), Info, '<no version>'),
  570    option(download(URL), Info, '<no download url>').
  571
  572search_info(title(_)).
  573search_info(version(_)).
  574search_info(download(_)).
  575
  576
  577                 /*******************************
  578                 *            INSTALL           *
  579                 *******************************/
  580
  581%!  pack_install(+Spec:atom) is det.
  582%!  pack_install(+SpecOrList, +Options) is det.
  583%
  584%   Install one or more packs from   SpecOrList.  SpecOrList is a single
  585%   specification or a list of specifications. A specification is one of
  586%
  587%     * A pack name.  This queries the pack repository
  588%       at https://www.swi-prolog.org
  589%     * Archive file name
  590%     * A http(s) URL of an archive file name.  This URL may contain a
  591%       star (*) for the version.  In this case pack_install/1 asks
  592%       for the directory content and selects the latest version.
  593%     * An https GIT URL
  594%     * A local directory name given as ``file://`` URL
  595%     * `'.'`, in which case a relative symlink is created to the
  596%       current directory (all other options for Spec make a copy
  597%       of the files).  Installation using a symlink is normally
  598%       used during development of a pack.
  599%
  600%   Processes the options below. Default  options   as  would be used by
  601%   pack_install/1 are used to complete the  provided Options. Note that
  602%   pack_install/2 can be used through the   SWI-Prolog command line app
  603%   `pack` as below. Most of the options of this predicate are available
  604%   as command line options.
  605%
  606%      swipl pack install <name>
  607%
  608%   Options:
  609%
  610%     * url(+URL)
  611%       Source for downloading the package
  612%     * pack_directory(+Dir)
  613%       Directory into which to install the package.
  614%     * global(+Boolean)
  615%       If `true`, install in the XDG common application data path,
  616%       making the pack accessible to everyone. If `false`, install in
  617%       the XDG user application data path, making the pack accessible
  618%       for the current user only. If the option is absent, use the
  619%       first existing and writable directory. If that doesn't exist
  620%       find locations where it can be created and prompt the user to do
  621%       so.
  622%     * insecure(+Boolean)
  623%       When `true` (default `false`), do not perform any checks on SSL
  624%       certificates when downloading using `https`.
  625%     * interactive(+Boolean)
  626%       Use default answer without asking the user if there
  627%       is a default action.
  628%     * silent(+Boolean)
  629%       If `true` (default false), suppress informational progress
  630%       messages.
  631%     * upgrade(+Boolean)
  632%       If `true` (default `false`), upgrade package if it is already
  633%       installed.
  634%     * rebuild(Condition)
  635%       Rebuild the foreign components.  Condition is one of
  636%       `if_absent` (default, do nothing if the directory with foreign
  637%       resources exists), `make` (run `make`) or `true` (run `make
  638%       distclean` followed by the default configure and build steps).
  639%     * test(Boolean)
  640%       If `true` (default), run the pack tests.
  641%     * git(+Boolean)
  642%       If `true` (default `false` unless `URL` ends with ``.git``),
  643%       assume the URL is a GIT repository.
  644%     * link(+Boolean)
  645%       Can be used if the installation source is a local directory
  646%       and the file system supports symbolic links.  In this case
  647%       the system adds the current directory to the pack registration
  648%       using a symbolic link and performs the local installation steps.
  649%     * version(+Version)
  650%       Demand the pack to satisfy some version requirement.  Version
  651%       is as defined by require_version/3.  For example `'1.5'` is the
  652%       same as `>=('1.5')`.
  653%     * branch(+Branch)
  654%       When installing from a git repository, clone this branch.
  655%     * commit(+Commit)
  656%       When installing from a git repository, checkout this commit.
  657%       Commit is either a hash, a tag, a branch or `'HEAD'`.
  658%     * build_type(+Type)
  659%       When building using CMake, use ``-DCMAKE_BUILD_TYPE=Type``.
  660%       Default is the build type of Prolog or ``Release``.
  661%     * register(+Boolean)
  662%       If `true` (default), register packages as downloaded after
  663%       performing the download.  This contacts the server with the
  664%       meta-data of each pack that was downloaded.  The server will
  665%       either register the location as a new version or increment
  666%       the download count.  The server stores the IP address of the
  667%       client.  Subsequent downloads of the same version from the
  668%       same IP address are ignored.
  669%     * server(+URL)
  670%       Pack server to contact. Default is the setting
  671%       `prolog_pack:server`, by default set to
  672%       ``https://www.swi-prolog.org/pack/``
  673%
  674%   Non-interactive installation can be established using the option
  675%   interactive(false). It is adviced to   install from a particular
  676%   _trusted_ URL instead of the  plain   pack  name  for unattented
  677%   operation.
  678
  679pack_install(Spec) :-
  680    pack_default_options(Spec, Pack, [], Options),
  681    pack_install(Pack, [pack(Pack)|Options]).
  682
  683pack_install(Specs, Options) :-
  684    is_list(Specs),
  685    !,
  686    maplist(pack_options(Options), Specs, Pairs),
  687    pack_install_dir(PackTopDir, Options),
  688    pack_install_set(Pairs, PackTopDir, Options).
  689pack_install(Spec, Options) :-
  690    pack_default_options(Spec, Pack, Options, DefOptions),
  691    (   option(already_installed(Installed), DefOptions)
  692    ->  print_message(informational, pack(already_installed(Installed)))
  693    ;   merge_options(Options, DefOptions, PackOptions),
  694        pack_install_dir(PackTopDir, PackOptions),
  695        pack_install_set([Pack-PackOptions], PackTopDir, Options)
  696    ).
  697
  698pack_options(Options, Spec, Pack-PackOptions) :-
  699    pack_default_options(Spec, Pack, Options, DefOptions),
  700    merge_options(Options, DefOptions, PackOptions).
  701
  702%!  pack_default_options(+Spec, -Pack, +OptionsIn, -Options) is det.
  703%
  704%   Establish  the  pack  name  (Pack)  and    install  options  from  a
  705%   specification and options (OptionsIn) provided by the user.  Cases:
  706%
  707%     1. Already installed.  We must pass that as pack_default_options/4
  708%        is called twice from pack_install/2.
  709%     2. Install from a URL due to a url(URL) option. Determine whether
  710%        the URL is a GIT repository, get the version and pack from the
  711%        URL.
  712%     3. Install a local archive file. Extract the pack and version from
  713%        the archive name.
  714%     4. Install from a git URL.  Determines the pack, sets git(true)
  715%        and adds the URL as option.
  716%     5. Install from a directory. Get the info from the `packs.pl`
  717%        file.
  718%     6. Install from `'.'`.  Create a symlink to make the current dir
  719%        accessible as a pack.
  720%     7. Install from a non-git URL
  721%        Determine pack and version.
  722%     8. Pack name.  Query the server to find candidate packs and
  723%        select an adequate pack.
  724
  725
  726pack_default_options(_Spec, Pack, OptsIn, Options) :-   % (1)
  727    option(already_installed(pack(Pack,_Version)), OptsIn),
  728    !,
  729    Options = OptsIn.
  730pack_default_options(_Spec, Pack, OptsIn, Options) :-   % (2)
  731    option(url(URL), OptsIn),
  732    !,
  733    (   option(git(_), OptsIn)
  734    ->  Options = OptsIn
  735    ;   git_url(URL, Pack)
  736    ->  Options = [git(true)|OptsIn]
  737    ;   Options = OptsIn
  738    ),
  739    (   nonvar(Pack)
  740    ->  true
  741    ;   option(pack(Pack), Options)
  742    ->  true
  743    ;   pack_version_file(Pack, _Version, URL)
  744    ).
  745pack_default_options(Archive, Pack, OptsIn, Options) :- % (3)
  746    must_be(atom, Archive),
  747    \+ uri_is_global(Archive),
  748    expand_file_name(Archive, [File]),
  749    exists_file(File),
  750    !,
  751    (   pack_version_file(Pack, Version, File)
  752    ->  uri_file_name(FileURL, File),
  753        merge_options([url(FileURL), version(Version)], OptsIn, Options)
  754    ;   domain_error(pack_file_name, Archive)
  755    ).
  756pack_default_options(URL, Pack, OptsIn, Options) :-     % (4)
  757    git_url(URL, Pack),
  758    !,
  759    merge_options([git(true), url(URL)], OptsIn, Options).
  760pack_default_options(FileURL, Pack, _, Options) :-      % (5)
  761    uri_file_name(FileURL, Dir),
  762    exists_directory(Dir),
  763    pack_info_term(Dir, name(Pack)),
  764    !,
  765    (   pack_info_term(Dir, version(Version))
  766    ->  uri_file_name(DirURL, Dir),
  767        Options = [url(DirURL), version(Version)]
  768    ;   throw(error(existence_error(key, version, Dir),_))
  769    ).
  770pack_default_options('.', Pack, OptsIn, Options) :-     % (6)
  771    pack_info_term('.', name(Pack)),
  772    !,
  773    working_directory(Dir, Dir),
  774    (   pack_info_term(Dir, version(Version))
  775    ->  uri_file_name(DirURL, Dir),
  776        NewOptions = [url(DirURL), version(Version) | Options1],
  777        (   current_prolog_flag(windows, true)
  778        ->  Options1 = []
  779        ;   Options1 = [link(true), rebuild(make)]
  780        ),
  781        merge_options(NewOptions, OptsIn, Options)
  782    ;   throw(error(existence_error(key, version, Dir),_))
  783    ).
  784pack_default_options(URL, Pack, OptsIn, Options) :-      % (7)
  785    pack_version_file(Pack, Version, URL),
  786    download_url(URL),
  787    !,
  788    available_download_versions(URL, Available, Options),
  789    Available = [URLVersion-LatestURL|_],
  790    NewOptions = [url(LatestURL)|VersionOptions],
  791    version_options(Version, URLVersion, Available, VersionOptions),
  792    merge_options(NewOptions, OptsIn, Options).
  793pack_default_options(Pack, Pack, Options, Options) :-    % (8)
  794    \+ uri_is_global(Pack).
  795
  796version_options(Version, Version, _, [version(Version)]) :- !.
  797version_options(Version, _, Available, [versions(Available)]) :-
  798    sub_atom(Version, _, _, _, *),
  799    !.
  800version_options(_, _, _, []).
  801
  802%!  pack_install_dir(-PackDir, +Options) is det.
  803%
  804%   Determine the directory below which to  install new packs. This find
  805%   or creates a writeable directory.  Options:
  806%
  807%     - pack_directory(+PackDir)
  808%       Use PackDir. PackDir is created if it does not exist.
  809%     - global(+Boolean)
  810%       If `true`, find a writeable global directory based on the
  811%       file search path `common_app_data`.  If `false`, find a
  812%       user-specific writeable directory based on `user_app_data`
  813%     - If neither of the above is given, use the search path
  814%       `pack`.
  815%
  816%   If no writeable directory is found, generate possible location where
  817%   this directory can be created and  ask   the  user  to create one of
  818%   them.
  819
  820pack_install_dir(PackDir, Options) :-
  821    option(pack_directory(PackDir), Options),
  822    ensure_directory(PackDir),
  823    !.
  824pack_install_dir(PackDir, Options) :-
  825    base_alias(Alias, Options),
  826    absolute_file_name(Alias, PackDir,
  827                       [ file_type(directory),
  828                         access(write),
  829                         file_errors(fail)
  830                       ]),
  831    !.
  832pack_install_dir(PackDir, Options) :-
  833    pack_create_install_dir(PackDir, Options).
  834
  835base_alias(Alias, Options) :-
  836    option(global(true), Options),
  837    !,
  838    Alias = common_app_data(pack).
  839base_alias(Alias, Options) :-
  840    option(global(false), Options),
  841    !,
  842    Alias = user_app_data(pack).
  843base_alias(Alias, _Options) :-
  844    Alias = pack('.').
  845
  846pack_create_install_dir(PackDir, Options) :-
  847    base_alias(Alias, Options),
  848    findall(Candidate = create_dir(Candidate),
  849            ( absolute_file_name(Alias, Candidate, [solutions(all)]),
  850              \+ exists_file(Candidate),
  851              \+ exists_directory(Candidate),
  852              file_directory_name(Candidate, Super),
  853              (   exists_directory(Super)
  854              ->  access_file(Super, write)
  855              ;   true
  856              )
  857            ),
  858            Candidates0),
  859    list_to_set(Candidates0, Candidates),   % keep order
  860    pack_create_install_dir(Candidates, PackDir, Options).
  861
  862pack_create_install_dir(Candidates, PackDir, Options) :-
  863    Candidates = [Default=_|_],
  864    !,
  865    append(Candidates, [cancel=cancel], Menu),
  866    menu(pack(create_pack_dir), Menu, Default, Selected, Options),
  867    Selected \== cancel,
  868    (   catch(make_directory_path(Selected), E,
  869              (print_message(warning, E), fail))
  870    ->  PackDir = Selected
  871    ;   delete(Candidates, PackDir=create_dir(PackDir), Remaining),
  872        pack_create_install_dir(Remaining, PackDir, Options)
  873    ).
  874pack_create_install_dir(_, _, _) :-
  875    print_message(error, pack(cannot_create_dir(pack(.)))),
  876    fail.
  877
  878%!  pack_unpack_from_local(+Source, +PackTopDir, +Name, -PackDir, +Options)
  879%
  880%   Unpack a package from a  local  media.   If  Source  is a directory,
  881%   either copy or link the directory. Else,   Source must be an archive
  882%   file. Options:
  883%
  884%      - link(+Boolean)
  885%        If the source is a directory, link or copy the directory?
  886%      - upgrade(true)
  887%        If the target is already there, wipe it and make a clean
  888%        install.
  889
  890pack_unpack_from_local(Source0, PackTopDir, Name, PackDir, Options) :-
  891    exists_directory(Source0),
  892    remove_slash(Source0, Source),
  893    !,
  894    directory_file_path(PackTopDir, Name, PackDir),
  895    (   option(link(true), Options)
  896    ->  (   same_file(Source, PackDir)
  897        ->  true
  898        ;   remove_existing_pack(PackDir, Options),
  899            atom_concat(PackTopDir, '/', PackTopDirS),
  900            relative_file_name(Source, PackTopDirS, RelPath),
  901            link_file(RelPath, PackDir, symbolic),
  902            assertion(same_file(Source, PackDir))
  903        )
  904    ;   \+ option(git(false), Options),
  905        is_git_directory(Source)
  906    ->  remove_existing_pack(PackDir, Options),
  907        run_process(path(git), [clone, Source, PackDir], [])
  908    ;   prepare_pack_dir(PackDir, Options),
  909        copy_directory(Source, PackDir)
  910    ).
  911pack_unpack_from_local(Source, PackTopDir, Name, PackDir, Options) :-
  912    exists_file(Source),
  913    directory_file_path(PackTopDir, Name, PackDir),
  914    prepare_pack_dir(PackDir, Options),
  915    pack_unpack(Source, PackDir, Name, Options).
  916
  917%!  pack_unpack(+SourceFile, +PackDir, +Pack, +Options)
  918%
  919%   Unpack an archive to the given package dir.
  920%
  921%   @tbd If library(archive) is  not  provided   we  could  check  for a
  922%   suitable external program such as `tar` or `unzip`.
  923
  924:- if(exists_source(library(archive))).  925pack_unpack(Source, PackDir, Pack, Options) :-
  926    ensure_loaded_archive,
  927    pack_archive_info(Source, Pack, _Info, StripOptions),
  928    prepare_pack_dir(PackDir, Options),
  929    archive_extract(Source, PackDir,
  930                    [ exclude(['._*'])          % MacOS resource forks
  931                    | StripOptions
  932                    ]).
  933:- else.  934pack_unpack(_,_,_,_) :-
  935    existence_error(library, archive).
  936:- endif.  937
  938%!  pack_install_local(:Spec, +Dir, +Options) is det.
  939%
  940%   Install a number of packages in   a  local directory. This predicate
  941%   supports installing packages local  to   an  application rather than
  942%   globally.
  943
  944pack_install_local(M:Gen, Dir, Options) :-
  945    findall(Pack-PackOptions, call(M:Gen, Pack, PackOptions), Pairs),
  946    pack_install_set(Pairs, Dir, Options).
  947
  948pack_install_set(Pairs, Dir, Options) :-
  949    must_be(list(pair), Pairs),
  950    ensure_directory(Dir),
  951    partition(known_media, Pairs, Local, Remote),
  952    maplist(pack_options_to_versions, Local, LocalVersions),
  953    (   Remote == []
  954    ->  AllVersions = LocalVersions
  955    ;   pairs_keys(Remote, Packs),
  956        prolog_description(Properties),
  957        query_pack_server(versions(Packs, Properties), Result, Options),
  958        (   Result = true(RemoteVersions)
  959        ->  append(LocalVersions, RemoteVersions, AllVersions)
  960        ;   print_message(error, pack(query_failed(Result))),
  961            fail
  962        )
  963    ),
  964    local_packs(Dir, Existing),
  965    pack_resolve(Pairs, Existing, AllVersions, Plan0, Options),
  966    !,                                      % for now, only first plan
  967    maplist(hsts_info(Options), Plan0, Plan),
  968    Options1 = [pack_directory(Dir), installed_packs(Existing)|Options],
  969    download_plan(Pairs, Plan, PlanB, Options1),
  970    register_downloads(PlanB, Options),
  971    maplist(update_automatic, PlanB),
  972    build_plan(PlanB, Built, Options1),
  973    publish_download(PlanB, Options),
  974    work_done(Pairs, Plan, PlanB, Built, Options).
  975
  976hsts_info(Options, Info0, Info) :-
  977    hsts(Info0.get(url), URL, Options),
  978    !,
  979    Info = Info0.put(url, URL).
  980hsts_info(_Options, Info, Info).
  981
  982%!  known_media(+Pair) is semidet.
  983%
  984%   True when the options specify installation   from  a known media. If
  985%   that applies to all packs, there is no  need to query the server. We
  986%   first  download  and  unpack  the  known  media,  then  examine  the
  987%   requirements and, if necessary, go to the server to resolve these.
  988
  989known_media(_-Options) :-
  990    option(url(_), Options).
  991
  992%!  pack_resolve(+Pairs, +Existing, +Versions, -Plan, +Options) is det.
  993%
  994%   Generate an installation plan. Pairs is a list of Pack-Options pairs
  995%   that  specifies  the  desired  packages.  Existing   is  a  list  of
  996%   pack(Pack, i, Title, Version, URL) terms that represents the already
  997%   installed packages. Versions  is  obtained   from  the  server.  See
  998%   `pack.pl` from the web server for  details. On success, this results
  999%   in a Plan to satisfies  the  requirements.   The  plan  is a list of
 1000%   packages to install with  their  location.   The  steps  satisfy the
 1001%   partial  ordering  of  dependencies,  such   that  dependencies  are
 1002%   installed before the dependents.  Options:
 1003%
 1004%     - upgrade(true)
 1005%       When specified, we try to install the latest version of all
 1006%       the packages.  Otherwise, we try to minimise the installation.
 1007
 1008pack_resolve(Pairs, Existing, Versions, Plan, Options) :-
 1009    insert_existing(Existing, Versions, AllVersions, Options),
 1010    phrase(select_version(Pairs, AllVersions,
 1011                          [ plan(PlanA),           % access to plan
 1012                            dependency_for([])     % dependencies
 1013                          | Options
 1014                          ]),
 1015           PlanA),
 1016    mark_installed(PlanA, Existing, Plan).
 1017
 1018%!  insert_existing(+Existing, +Available, -Candidates, +Options) is det.
 1019%
 1020%   Combine the already existing packages  with   the  ones  reported as
 1021%   available by the server to a list of Candidates, where the candidate
 1022%   of  each  package  is   ordered    according   by  preference.  When
 1023%   upgrade(true) is specified, the existing is   merged into the set of
 1024%   Available versions. Otherwise Existing is prepended to Available, so
 1025%   it is selected as first.
 1026
 1027:- det(insert_existing/4). 1028insert_existing(Existing, [], Versions, _Options) =>
 1029    maplist(existing_to_versions, Existing, Versions).
 1030insert_existing(Existing, [Pack-Versions|T0], AllPackVersions, Options),
 1031    select(Installed, Existing, Existing2),
 1032    Installed.pack == Pack =>
 1033    can_upgrade(Installed, Versions, Installed2),
 1034    insert_existing_(Installed2, Versions, AllVersions, Options),
 1035    AllPackVersions = [Pack-AllVersions|T],
 1036    insert_existing(Existing2, T0, T, Options).
 1037insert_existing(Existing, [H|T0], AllVersions, Options) =>
 1038    AllVersions = [H|T],
 1039    insert_existing(Existing, T0, T, Options).
 1040
 1041existing_to_versions(Installed, Pack-[Version-[Installed]]) :-
 1042    Pack = Installed.pack,
 1043    Version = Installed.version.
 1044
 1045insert_existing_(Installed, Versions, AllVersions, Options) :-
 1046    option(upgrade(true), Options),
 1047    !,
 1048    insert_existing_(Installed, Versions, AllVersions).
 1049insert_existing_(Installed, Versions, AllVersions, _) :-
 1050    AllVersions = [Installed.version-[Installed]|Versions].
 1051
 1052insert_existing_(Installed, [H|T0], [H|T]) :-
 1053    H = V0-_Infos,
 1054    cmp_versions(>, V0, Installed.version),
 1055    !,
 1056    insert_existing_(Installed, T0, T).
 1057insert_existing_(Installed, [H0|T], [H|T]) :-
 1058    H0 = V0-Infos,
 1059    V0 == Installed.version,
 1060    !,
 1061    H = V0-[Installed|Infos].
 1062insert_existing_(Installed, Versions, All) :-
 1063    All =  [Installed.version-[Installed]|Versions].
 1064
 1065%!  can_upgrade(+Installed, +Versions, -Installed2) is det.
 1066%
 1067%   Add a `latest_version` key to Installed if its version is older than
 1068%   the latest available version.
 1069
 1070can_upgrade(Info, [Version-_|_], Info2) :-
 1071    cmp_versions(>, Version, Info.version),
 1072    !,
 1073    Info2 = Info.put(latest_version, Version).
 1074can_upgrade(Info, _, Info).
 1075
 1076%!  mark_installed(+PlanA, +Existing, -Plan) is det.
 1077%
 1078%   Mark  already  up-to-date  packs  from  the   plan  and  add  a  key
 1079%   `upgrade:true` to elements of PlanA  in   Existing  that are not the
 1080%   same.
 1081
 1082mark_installed([], _, []).
 1083mark_installed([Info|T], Existing, Plan) :-
 1084    (   member(Installed, Existing),
 1085        Installed.pack == Info.pack
 1086    ->  (   (   Installed.git == true
 1087            ->  Info.git == true,
 1088                Installed.hash == Info.hash
 1089            ;   Version = Info.get(version)
 1090            ->  Installed.version == Version
 1091            )
 1092        ->  Plan = [Info.put(keep, true)|PlanT]    % up-to-date
 1093        ;   Plan = [Info.put(upgrade, Installed)|PlanT] % needs upgrade
 1094        )
 1095    ;   Plan = [Info|PlanT]                        % new install
 1096    ),
 1097    mark_installed(T, Existing, PlanT).
 1098
 1099%!  select_version(+PackAndOptions, +Available, +Options)// is nondet.
 1100%
 1101%   True when the output is a list of   pack info dicts that satisfy the
 1102%   installation requirements of PackAndOptions from  the packs known to
 1103%   be Available.
 1104
 1105select_version([], _, _) -->
 1106    [].
 1107select_version([Pack-PackOptions|More], Versions, Options) -->
 1108    { memberchk(Pack-PackVersions, Versions),
 1109      member(Version-Infos, PackVersions),
 1110      compatible_version(Pack, Version, PackOptions),
 1111      member(Info, Infos),
 1112      pack_options_compatible_with_info(Info, PackOptions),
 1113      pack_satisfies(Pack, Version, Info, Info2, PackOptions),
 1114      all_downloads(PackVersions, Downloads)
 1115    },
 1116    add_to_plan(Info2.put(_{version: Version, all_downloads:Downloads}),
 1117                Versions, Options),
 1118    select_version(More, Versions, Options).
 1119select_version([Pack-_PackOptions|_More], _Versions, _Options) -->
 1120    { existence_error(pack, Pack) }.               % or warn and continue?
 1121
 1122all_downloads(PackVersions, AllDownloads) :-
 1123    aggregate_all(sum(Downloads),
 1124                  ( member(_Version-Infos, PackVersions),
 1125                    member(Info, Infos),
 1126                    get_dict(downloads, Info, Downloads)
 1127                  ),
 1128                  AllDownloads).
 1129
 1130add_requirements([], _, _) -->
 1131    [].
 1132add_requirements([H|T], Versions, Options) -->
 1133    { is_prolog_token(H),
 1134      !,
 1135      prolog_satisfies(H)
 1136    },
 1137    add_requirements(T, Versions, Options).
 1138add_requirements([H|T], Versions, Options) -->
 1139    { member(Pack-PackVersions, Versions),
 1140      member(Version-Infos, PackVersions),
 1141      member(Info, Infos),
 1142      (   Provides = @(Pack,Version)
 1143      ;   member(Provides, Info.get(provides))
 1144      ),
 1145      satisfies_req(Provides, H),
 1146      all_downloads(PackVersions, Downloads)
 1147    },
 1148    add_to_plan(Info.put(_{version: Version, all_downloads:Downloads}),
 1149                Versions, Options),
 1150    add_requirements(T, Versions, Options).
 1151
 1152%!  add_to_plan(+Info, +Versions, +Options) is semidet.
 1153%
 1154%   Add Info to the plan. If an Info   about the same pack is already in
 1155%   the plan, but this is a different version  of the pack, we must fail
 1156%   as we cannot install two different versions of a pack.
 1157
 1158add_to_plan(Info, _Versions, Options) -->
 1159    { option(plan(Plan), Options),
 1160      member_nonvar(Planned, Plan),
 1161      Planned.pack == Info.pack,
 1162      !,
 1163      same_version(Planned, Info)                  % same pack, different version
 1164    }.
 1165add_to_plan(Info, _Versions, _Options) -->
 1166    { member(Conflict, Info.get(conflicts)),
 1167      is_prolog_token(Conflict),
 1168      prolog_satisfies(Conflict),
 1169      !,
 1170      fail                                         % incompatible with this Prolog
 1171    }.
 1172add_to_plan(Info, _Versions, Options) -->
 1173    { option(plan(Plan), Options),
 1174      member_nonvar(Planned, Plan),
 1175      info_conflicts(Info, Planned),               % Conflicts with a planned pack
 1176      !,
 1177      fail
 1178    }.
 1179add_to_plan(Info, Versions, Options) -->
 1180    { select_option(dependency_for(Dep0), Options, Options1),
 1181      Options2 = [dependency_for([Info.pack|Dep0])|Options1],
 1182      (   Dep0 = [DepFor|_]
 1183      ->  add_dependency_for(DepFor, Info, Info1)
 1184      ;   Info1 = Info
 1185      )
 1186    },
 1187    [Info1],
 1188    add_requirements(Info.get(requires,[]), Versions, Options2).
 1189
 1190add_dependency_for(Pack, Info, Info) :-
 1191    Old = Info.get(dependency_for),
 1192    !,
 1193    b_set_dict(dependency_for, Info, [Pack|Old]).
 1194add_dependency_for(Pack, Info0, Info) :-
 1195    Info = Info0.put(dependency_for, [Pack]).
 1196
 1197same_version(Info, Info) :-
 1198    !.
 1199same_version(Planned, Info) :-
 1200    Hash = Planned.get(hash),
 1201    Hash \== (-),
 1202    !,
 1203    Hash == Info.get(hash).
 1204same_version(Planned, Info) :-
 1205    Planned.get(version) == Info.get(version).
 1206
 1207%!  info_conflicts(+Info1, +Info2) is semidet.
 1208%
 1209%   True if Info2 is in conflict with Info2. The relation is symetric.
 1210
 1211info_conflicts(Info, Planned) :-
 1212    info_conflicts_(Info, Planned),
 1213    !.
 1214info_conflicts(Info, Planned) :-
 1215    info_conflicts_(Planned, Info),
 1216    !.
 1217
 1218info_conflicts_(Info, Planned) :-
 1219    member(Conflict, Info.get(conflicts)),
 1220    \+ is_prolog_token(Conflict),
 1221    info_provides(Planned, Provides),
 1222    satisfies_req(Provides, Conflict),
 1223    !.
 1224
 1225info_provides(Info, Provides) :-
 1226    (   Provides = Info.pack@Info.version
 1227    ;   member(Provides, Info.get(provides))
 1228    ).
 1229
 1230%!  pack_satisfies(+Pack, +Version, +Info0, -Info, +Options) is semidet.
 1231%
 1232%   True if Pack@Version  with  Info   satisfies  the  pack installation
 1233%   options provided by Options.
 1234
 1235pack_satisfies(_Pack, _Version, Info0, Info, Options) :-
 1236    option(commit('HEAD'), Options),
 1237    !,
 1238    Info0.get(git) == true,
 1239    Info = Info0.put(commit, 'HEAD').
 1240pack_satisfies(_Pack, _Version, Info, Info, Options) :-
 1241    option(commit(Commit), Options),
 1242    !,
 1243    Commit == Info.get(hash).
 1244pack_satisfies(Pack, Version, Info, Info, Options) :-
 1245    option(version(ReqVersion), Options),
 1246    !,
 1247    satisfies_version(Pack, Version, ReqVersion).
 1248pack_satisfies(_Pack, _Version, Info, Info, _Options).
 1249
 1250%!  satisfies_version(+Pack, +PackVersion, +RequiredVersion) is semidet.
 1251
 1252satisfies_version(Pack, Version, ReqVersion) :-
 1253    catch(require_version(pack(Pack), Version, ReqVersion),
 1254          error(version_error(pack(Pack), Version, ReqVersion),_),
 1255          fail).
 1256
 1257%!  satisfies_req(+Provides, +Required) is semidet.
 1258%
 1259%   Check a token requirements.
 1260
 1261satisfies_req(Token, Token) => true.
 1262satisfies_req(@(Token,_), Token) => true.
 1263satisfies_req(@(Token,PrvVersion), Req), cmp(Req, Token, Cmp, ReqVersion) =>
 1264	cmp_versions(Cmp, PrvVersion, ReqVersion).
 1265satisfies_req(_,_) => fail.
 1266
 1267cmp(Token  < Version, Token, <,	 Version).
 1268cmp(Token =< Version, Token, =<, Version).
 1269cmp(Token =  Version, Token, =,	 Version).
 1270cmp(Token == Version, Token, ==, Version).
 1271cmp(Token >= Version, Token, >=, Version).
 1272cmp(Token >  Version, Token, >,	 Version).
 1273
 1274%!  pack_options_to_versions(+PackOptionsPair, -Versions) is det.
 1275%
 1276%   Create an available  package  term  from   Pack  and  Options  if it
 1277%   contains a url(URL) option. This allows installing packages that are
 1278%   not known to the server. In most cases, the URL will be a git URL or
 1279%   the URL to download an archive. It can  also be a ``file://`` url to
 1280%   install from a local archive.
 1281%
 1282%   The   first   clause   deals    with     a    wildcard    URL.   See
 1283%   pack_default_options/4, case (7).
 1284
 1285:- det(pack_options_to_versions/2). 1286pack_options_to_versions(Pack-PackOptions, Pack-Versions) :-
 1287    option(versions(Available), PackOptions), !,
 1288    maplist(version_url_info(Pack, PackOptions), Available, Versions).
 1289pack_options_to_versions(Pack-PackOptions, Pack-[Version-[Info]]) :-
 1290    option(url(URL), PackOptions),
 1291    findall(Prop, option_info_prop(PackOptions, Prop), Pairs),
 1292    dict_create(Info, #,
 1293                [ pack-Pack,
 1294                  url-URL
 1295                | Pairs
 1296                ]),
 1297    Version = Info.get(version, '0.0.0').
 1298
 1299version_url_info(Pack, PackOptions, Version-URL, Version-[Info]) :-
 1300    findall(Prop,
 1301            ( option_info_prop(PackOptions, Prop),
 1302              Prop \= version-_
 1303            ),
 1304            Pairs),
 1305    dict_create(Info, #,
 1306                [ pack-Pack,
 1307                  url-URL,
 1308                  version-Version
 1309                | Pairs
 1310                ]).
 1311
 1312option_info_prop(PackOptions, Prop-Value) :-
 1313    option_info(Prop),
 1314    Opt =.. [Prop,Value],
 1315    option(Opt, PackOptions).
 1316
 1317option_info(git).
 1318option_info(hash).
 1319option_info(version).
 1320option_info(branch).
 1321option_info(link).
 1322
 1323%!  compatible_version(+Pack, +Version, +Options) is semidet.
 1324%
 1325%   Fails if Options demands a  version   and  Version is not compatible
 1326%   with Version.
 1327
 1328compatible_version(Pack, Version, PackOptions) :-
 1329    option(version(ReqVersion), PackOptions),
 1330    !,
 1331    satisfies_version(Pack, Version, ReqVersion).
 1332compatible_version(_, _, _).
 1333
 1334%!  pack_options_compatible_with_info(+Info, +PackOptions) is semidet.
 1335%
 1336%   Ignore information from the server  that   is  incompatible with the
 1337%   request.
 1338
 1339pack_options_compatible_with_info(Info, PackOptions) :-
 1340    findall(Prop, option_info_prop(PackOptions, Prop), Pairs),
 1341    dict_create(Dict, _, Pairs),
 1342    Dict >:< Info.
 1343
 1344%!  download_plan(+Targets, +Plan, +Options) is semidet.
 1345%
 1346%   Download or update all packages from Plan. We   need to do this as a
 1347%   first  step  because  we  may    not  have  (up-to-date)  dependency
 1348%   information about all packs. For example, a pack may be installed at
 1349%   the git HEAD revision that is not yet  known to the server or it may
 1350%   be installed from a url that is not known at all at the server.
 1351
 1352download_plan(_Targets, Plan, Plan, _Options) :-
 1353    exclude(installed, Plan, []),
 1354    !.
 1355download_plan(Targets, Plan0, Plan, Options) :-
 1356    confirm(download_plan(Plan0), yes, Options),
 1357    maplist(download_from_info(Options), Plan0, Plan1),
 1358    plan_unsatisfied_dependencies(Plan1, Deps),
 1359    (   Deps == []
 1360    ->  Plan = Plan1
 1361    ;   print_message(informational, pack(new_dependencies(Deps))),
 1362        prolog_description(Properties),
 1363        query_pack_server(versions(Deps, Properties), Result, []),
 1364        (   Result = true(Versions)
 1365        ->  merge_installed(Plan1, Options, Existing),
 1366            pack_resolve(Targets, Existing, Versions, Plan2, Options),
 1367            !,
 1368            download_plan(Targets, Plan2, Plan, Options)
 1369        ;   print_message(error, pack(query_failed(Result))),
 1370            fail
 1371        )
 1372    ).
 1373
 1374%!  merge_installed(+Plan, +Options, -Existing) is det.
 1375%
 1376%   Combine the just-processed Plan with the   locally installed packs
 1377%   passed through Options as `installed_packs(Installed)`, so that the
 1378%   recursive dependency resolution considers   already installed packs
 1379%   able  to  satisfy   requirements.    Packs   in  Plan  shadow  same
 1380%   pack in Installed.
 1381
 1382merge_installed(Plan, Options, Existing) :-
 1383    option(installed_packs(Installed), Options, []),
 1384    exclude(in_plan(Plan), Installed, InstalledRest),
 1385    append(Plan, InstalledRest, Existing).
 1386
 1387in_plan(Plan, Info) :-
 1388    member(P, Plan),
 1389    P.pack == Info.pack,
 1390    !.
 1391
 1392%!  plan_unsatisfied_dependencies(+Plan, -Deps) is det.
 1393%
 1394%   True when Deps is a list of dependency   tokens  in Plan that is not
 1395%   satisfied.
 1396
 1397plan_unsatisfied_dependencies(Plan, Deps) :-
 1398    phrase(plan_unsatisfied_dependencies(Plan, Plan), Deps).
 1399
 1400plan_unsatisfied_dependencies([], _) -->
 1401    [].
 1402plan_unsatisfied_dependencies([Info|Infos], Plan) -->
 1403    { Deps = Info.get(requires) },
 1404    plan_unsatisfied_requirements(Deps, Plan),
 1405    plan_unsatisfied_dependencies(Infos, Plan).
 1406
 1407plan_unsatisfied_requirements([], _) -->
 1408    [].
 1409plan_unsatisfied_requirements([H|T], Plan) -->
 1410    { is_prolog_token(H),           % Can this fail?
 1411      prolog_satisfies(H)
 1412    },
 1413    !,
 1414    plan_unsatisfied_requirements(T, Plan).
 1415plan_unsatisfied_requirements([H|T], Plan) -->
 1416    { member(Info, Plan),
 1417      (   (   Version = Info.get(version)
 1418          ->  Provides = @(Info.get(pack), Version)
 1419          ;   Provides = Info.get(pack)
 1420          )
 1421      ;   member(Provides, Info.get(provides))
 1422      ),
 1423      satisfies_req(Provides, H)
 1424    }, !,
 1425    plan_unsatisfied_requirements(T, Plan).
 1426plan_unsatisfied_requirements([H|T], Plan) -->
 1427    [H],
 1428    plan_unsatisfied_requirements(T, Plan).
 1429
 1430
 1431%!  build_plan(+Plan, -Built, +Options) is det.
 1432%
 1433%   Run post installation steps.  We   build  dependencies  before their
 1434%   dependents, so we first do a topological  sort on the packs based on
 1435%   the pack dependencies.
 1436
 1437build_plan(Plan, Ordered, Options) :-
 1438    maplist(decide_autoload_pack(Options), Plan, Plan1),
 1439    partition(needs_rebuild_from_info(Options), Plan1, ToBuild, NoBuild),
 1440    maplist(attach_from_info(Options), NoBuild),
 1441    (   ToBuild == []
 1442    ->  post_install_autoload(NoBuild),
 1443        Ordered = []
 1444    ;   order_builds(ToBuild, Ordered),
 1445        confirm(build_plan(Ordered), yes, Options),
 1446        maplist(exec_plan_rebuild_step(Options), Ordered)
 1447    ).
 1448
 1449%!  needs_rebuild_from_info(+Options, +Info) is semidet.
 1450%
 1451%   True when we need to rebuilt the pack.
 1452
 1453needs_rebuild_from_info(Options, Info) :-
 1454    PackDir = Info.installed,
 1455    is_foreign_pack(PackDir, _),
 1456    \+ is_built(PackDir, Options).
 1457
 1458%!  is_built(+PackDir, +Options) is semidet.
 1459%
 1460%   True if the pack in PackDir has been built.
 1461%
 1462%   @tbd We now verify it was built by   the exact same version. That is
 1463%   normally an overkill.
 1464
 1465is_built(PackDir, _Options) :-
 1466    current_prolog_flag(arch, Arch),
 1467    prolog_version_dotted(Version), % Major.Minor.Patch
 1468    pack_status_dir(PackDir, built(Arch, Version, _)).
 1469
 1470%!  order_builds(+ToBuild, -Ordered) is det.
 1471%
 1472%   Order the build  processes  by   building  dependencies  before  the
 1473%   packages that rely on them as they may need them during the build.
 1474
 1475order_builds(ToBuild, Ordered) :-
 1476    findall(Pack-Dependent, dep_edge(ToBuild, Pack, Dependent), Edges),
 1477    maplist(get_dict(pack), ToBuild, Packs),
 1478    vertices_edges_to_ugraph(Packs, Edges, Graph),
 1479    ugraph_layers(Graph, Layers),
 1480    append(Layers, PackNames),
 1481    maplist(pack_info_from_name(ToBuild), PackNames, Ordered).
 1482
 1483%!  dep_edge(+Infos, -Pack, -Dependent) is nondet.
 1484%
 1485%   True when Pack needs to be installed   as a dependency of Dependent.
 1486%   Both Pack and Dependent are pack _names_. I.e., this implies that we
 1487%   must build Pack _before_ Dependent.
 1488
 1489dep_edge(Infos, Pack, Dependent) :-
 1490    member(Info, Infos),
 1491    Pack = Info.pack,
 1492    member(Dependent, Info.get(dependency_for)),
 1493    (   member(DepInfo, Infos),
 1494        DepInfo.pack == Dependent
 1495    ->  true
 1496    ).
 1497
 1498:- det(pack_info_from_name/3). 1499pack_info_from_name(Infos, Pack, Info) :-
 1500    member(Info, Infos),
 1501    Info.pack == Pack,
 1502    !.
 1503
 1504%!  exec_plan_rebuild_step(+Options, +Info) is det.
 1505%
 1506%   Execute the rebuild steps for the given Info.
 1507
 1508exec_plan_rebuild_step(Options, Info) :-
 1509    print_message(informational, pack(build(Info.pack, Info.installed))),
 1510    pack_post_install(Info, Options),
 1511    attach_from_info(Options, Info).
 1512
 1513%!  attach_from_info(+Options, +Info) is det.
 1514%
 1515%   Make the package visible.
 1516
 1517attach_from_info(_Options, Info) :-
 1518    Info.get(keep) == true,
 1519    !.
 1520attach_from_info(Options, Info) :-
 1521    (   option(pack_directory(_Parent), Options)
 1522    ->  pack_attach(Info.installed, [duplicate(replace)])
 1523    ;   pack_attach(Info.installed, [])
 1524    ).
 1525
 1526%!  download_from_info(+Options, +Info0, -Info) is det.
 1527%
 1528%   Download a package guided by Info. Note   that this does __not__ run
 1529%   any scripts. This implies that dependencies do not matter and we can
 1530%   proceed in any order. This is important  because we may use packages
 1531%   at their git HEAD, which implies  that requirements may be different
 1532%   from what is in the Info terms.
 1533
 1534download_from_info(Options, Info0, Info), option(dryrun(true), Options) =>
 1535    print_term(Info0, [nl(true)]),
 1536    Info = Info0.
 1537download_from_info(_Options, Info0, Info), installed(Info0) =>
 1538    Info = Info0.
 1539download_from_info(_Options, Info0, Info),
 1540    _{upgrade:OldInfo, git:true} :< Info0,
 1541    is_git_directory(OldInfo.installed) =>
 1542    PackDir = OldInfo.installed,
 1543    git_checkout_version(PackDir, [commit(Info0.hash)]),
 1544    reload_info(PackDir, Info0, Info).
 1545download_from_info(Options, Info0, Info),
 1546    _{upgrade:OldInfo} :< Info0 =>
 1547    PackDir = OldInfo.installed,
 1548    detach_pack(OldInfo.pack, PackDir),
 1549    delete_directory_and_contents(PackDir),
 1550    del_dict(upgrade, Info0, _, Info1),
 1551    download_from_info(Options, Info1, Info).
 1552download_from_info(Options, Info0, Info),
 1553    _{url:URL, git:true} :< Info0, \+ have_git =>
 1554    git_archive_url(URL, Archive, Options),
 1555    download_from_info([git_url(URL)|Options],
 1556                       Info0.put(_{ url:Archive,
 1557                                    git:false,
 1558                                    git_url:URL
 1559                                  }),
 1560                       Info1),
 1561                                % restore the hash to register the download.
 1562    (   Info1.get(version) == Info0.get(version),
 1563        Hash = Info0.get(hash)
 1564    ->  Info = Info1.put(hash, Hash)
 1565    ;   Info = Info1
 1566    ).
 1567download_from_info(Options, Info0, Info),
 1568    _{url:URL} :< Info0 =>
 1569    select_option(pack_directory(Dir), Options, Options1),
 1570    select_option(version(_), Options1, Options2, _),
 1571    download_info_extra(Info0, InstallOptions, Options2),
 1572    pack_download_from_url(URL, Dir, Info0.pack,
 1573                           [ interactive(false),
 1574                             pack_dir(PackDir)
 1575                           | InstallOptions
 1576                           ]),
 1577    reload_info(PackDir, Info0, Info).
 1578
 1579download_info_extra(Info, [git(true),commit(Hash)|Options], Options) :-
 1580    Info.get(git) == true,
 1581    !,
 1582    Hash = Info.get(commit, 'HEAD').
 1583download_info_extra(Info, [link(true)|Options], Options) :-
 1584    Info.get(link) == true,
 1585    !.
 1586download_info_extra(_, Options, Options).
 1587
 1588installed(Info) :-
 1589    _ = Info.get(installed).
 1590
 1591detach_pack(Pack, PackDir) :-
 1592    (   current_pack(Pack, PackDir)
 1593    ->  '$pack_detach'(Pack, PackDir)
 1594    ;   true
 1595    ).
 1596
 1597%!  reload_info(+PackDir, +Info0, -Info) is det.
 1598%
 1599%   Update the requires and provides metadata. Info0 is what we got from
 1600%   the server, but the package may be   different  as we may have asked
 1601%   for the git HEAD or the package URL   may not have been known by the
 1602%   server at all.
 1603
 1604reload_info(_PackDir, Info, Info) :-
 1605    _ = Info.get(installed),	% we read it from the package
 1606    !.
 1607reload_info(PackDir, Info0, Info) :-
 1608    local_pack_info(PackDir, Info1),
 1609    Info = Info0.put(installed, PackDir)
 1610                .put(downloaded, Info0.url)
 1611                .put(Info1).
 1612
 1613%!  work_done(+Targets, +Plan, +PlanB, +Built, +Options) is det.
 1614%
 1615%   Targets has successfully been installed  and   the  packs Built have
 1616%   successfully ran their build scripts.
 1617
 1618work_done(_, _, _, _, Options),
 1619    option(silent(true), Options) =>
 1620    true.
 1621work_done(Targets, Plan, Plan, [], _Options) =>
 1622    convlist(can_upgrade_target(Plan), Targets, CanUpgrade),
 1623    (   CanUpgrade == []
 1624    ->  pairs_keys(Targets, Packs),
 1625        print_message(informational, pack(up_to_date(Packs)))
 1626    ;   print_message(informational, pack(installed_can_upgrade(CanUpgrade)))
 1627    ).
 1628work_done(_, _, _, _, _) =>
 1629    true.
 1630
 1631can_upgrade_target(Plan, Pack-_, Info) =>
 1632    member(Info, Plan),
 1633    Info.pack == Pack,
 1634    !,
 1635    _ = Info.get(latest_version).
 1636
 1637%!  local_packs(+Dir, -Packs) is det.
 1638%
 1639%   True when Packs  is  a  list   with  information  for  all installed
 1640%   packages.
 1641
 1642local_packs(Dir, Packs) :-
 1643    findall(Pack, pack_in_subdir(Dir, Pack), Packs).
 1644
 1645pack_in_subdir(Dir, Info) :-
 1646    directory_member(Dir, PackDir,
 1647                     [ file_type(directory),
 1648                       hidden(false)
 1649                     ]),
 1650    local_pack_info(PackDir, Info).
 1651
 1652local_pack_info(PackDir,
 1653                #{ pack: Pack,
 1654                   version: Version,
 1655                   title: Title,
 1656                   hash: Hash,
 1657                   url: URL,
 1658                   git: IsGit,
 1659                   requires: Requires,
 1660                   provides: Provides,
 1661                   conflicts: Conflicts,
 1662                   installed: PackDir
 1663                 }) :-
 1664    directory_file_path(PackDir, 'pack.pl', MetaFile),
 1665    exists_file(MetaFile),
 1666    file_base_name(PackDir, DirName),
 1667    findall(Term, pack_dir_info(PackDir, _, Term), Info),
 1668    option(pack(Pack), Info, DirName),
 1669    option(title(Title), Info, '<no title>'),
 1670    option(version(Version), Info, '<no version>'),
 1671    option(download(URL), Info, '<no download url>'),
 1672    findall(Req, member(requires(Req), Info), Requires),
 1673    findall(Prv, member(provides(Prv), Info), Provides),
 1674    findall(Cfl, member(conflicts(Cfl), Info), Conflicts),
 1675    (   have_git,
 1676        is_git_directory(PackDir)
 1677    ->  git_hash(Hash, [directory(PackDir)]),
 1678        IsGit = true
 1679    ;   Hash = '-',
 1680        IsGit = false
 1681    ).
 1682
 1683
 1684		 /*******************************
 1685		 *        PROLOG VERSIONS	*
 1686		 *******************************/
 1687
 1688%!  prolog_description(-Description) is det.
 1689%
 1690%   Provide a description of the running Prolog system. Version terms:
 1691%
 1692%     - prolog(Dialect, Version)
 1693%
 1694%   @tbd:   establish   a   language    for     features.    Sync   with
 1695%   library(prolog_versions)
 1696
 1697prolog_description([prolog(swi(Version))]) :-
 1698    prolog_version(Version).
 1699
 1700prolog_version(Version) :-
 1701    current_prolog_flag(version_git, Version),
 1702    !.
 1703prolog_version(Version) :-
 1704    prolog_version_dotted(Version).
 1705
 1706prolog_version_dotted(Version) :-
 1707    current_prolog_flag(version_data, swi(Major, Minor, Patch, _)),
 1708    VNumbers = [Major, Minor, Patch],
 1709    atomic_list_concat(VNumbers, '.', Version).
 1710
 1711%!  is_prolog_token(+Token) is semidet.
 1712%
 1713%   True when Token describes a property of the target Prolog
 1714%   system.
 1715
 1716is_prolog_token(Token), cmp(Token, prolog, _Cmp, _Version) => true.
 1717is_prolog_token(prolog:Feature), atom(Feature) => true.
 1718is_prolog_token(prolog:Feature), flag_value_feature(Feature, _Flag, _Value) =>
 1719    true.
 1720is_prolog_token(_) => fail.
 1721
 1722%!  prolog_satisfies(+Token) is semidet.
 1723%
 1724%   True when the  running  Prolog   system  satisfies  token. Processes
 1725%   requires(Token) terms for
 1726%
 1727%     - prolog Cmp Version
 1728%       Demand a Prolog version (range).
 1729%     - prolog:Flag
 1730%     - prolog:Flag(Value)
 1731%     - prolog:library(Lib)
 1732%
 1733%   @see require_prolog_version/2.
 1734
 1735prolog_satisfies(Token), cmp(Token, prolog, Cmp, ReqVersion) =>
 1736    prolog_version(CurrentVersion),
 1737    cmp_versions(Cmp, CurrentVersion, ReqVersion).
 1738prolog_satisfies(prolog:library(Lib)), atom(Lib) =>
 1739    exists_source(library(Lib)).
 1740prolog_satisfies(prolog:Feature), atom(Feature) =>
 1741    current_prolog_flag(Feature, true).
 1742prolog_satisfies(prolog:Feature), flag_value_feature(Feature, Flag, Value) =>
 1743    current_prolog_flag(Flag, Value).
 1744
 1745flag_value_feature(Feature, Flag, Value) :-
 1746    compound(Feature),
 1747    compound_name_arguments(Feature, Flag, [Value]),
 1748    atom(Flag).
 1749
 1750
 1751                 /*******************************
 1752                 *             INFO             *
 1753                 *******************************/
 1754
 1755%!  pack_archive_info(+Archive, +Pack, -Info, -Strip)
 1756%
 1757%   True when Archive archives Pack. Info  is unified with the terms
 1758%   from pack.pl in the  pack  and   Strip  is  the strip-option for
 1759%   archive_extract/3.
 1760%
 1761%   Requires library(archive), which is lazily loaded when needed.
 1762%
 1763%   @error  existence_error(pack_file, 'pack.pl') if the archive
 1764%           doesn't contain pack.pl
 1765%   @error  Syntax errors if pack.pl cannot be parsed.
 1766
 1767:- if(exists_source(library(archive))). 1768ensure_loaded_archive :-
 1769    current_predicate(archive_open/3),
 1770    !.
 1771ensure_loaded_archive :-
 1772    use_module(library(archive)).
 1773
 1774pack_archive_info(Archive, Pack, [archive_size(Bytes)|Info], Strip) :-
 1775    ensure_loaded_archive,
 1776    size_file(Archive, Bytes),
 1777    setup_call_cleanup(
 1778        archive_open(Archive, Handle, []),
 1779        (   repeat,
 1780            (   archive_next_header(Handle, InfoFile)
 1781            ->  true
 1782            ;   !, fail
 1783            )
 1784        ),
 1785        archive_close(Handle)),
 1786    file_base_name(InfoFile, 'pack.pl'),
 1787    atom_concat(Prefix, 'pack.pl', InfoFile),
 1788    strip_option(Prefix, Pack, Strip),
 1789    setup_call_cleanup(
 1790        archive_open_entry(Handle, Stream),
 1791        read_stream_to_terms(Stream, Info),
 1792        close(Stream)),
 1793    !,
 1794    must_be(ground, Info),
 1795    maplist(valid_term(pack_info_term), Info).
 1796:- else. 1797pack_archive_info(_, _, _, _) :-
 1798    existence_error(library, archive).
 1799:- endif. 1800pack_archive_info(_, _, _, _) :-
 1801    existence_error(pack_file, 'pack.pl').
 1802
 1803strip_option('', _, []) :- !.
 1804strip_option('./', _, []) :- !.
 1805strip_option(Prefix, Pack, [remove_prefix(Prefix)]) :-
 1806    atom_concat(PrefixDir, /, Prefix),
 1807    file_base_name(PrefixDir, Base),
 1808    (   Base == Pack
 1809    ->  true
 1810    ;   pack_version_file(Pack, _, Base)
 1811    ->  true
 1812    ;   \+ sub_atom(PrefixDir, _, _, _, /)
 1813    ).
 1814
 1815read_stream_to_terms(Stream, Terms) :-
 1816    read(Stream, Term0),
 1817    read_stream_to_terms(Term0, Stream, Terms).
 1818
 1819read_stream_to_terms(end_of_file, _, []) :- !.
 1820read_stream_to_terms(Term0, Stream, [Term0|Terms]) :-
 1821    read(Stream, Term1),
 1822    read_stream_to_terms(Term1, Stream, Terms).
 1823
 1824
 1825%!  pack_git_info(+GitDir, -Hash, -Info) is det.
 1826%
 1827%   Retrieve info from a cloned git   repository  that is compatible
 1828%   with pack_archive_info/4.
 1829
 1830pack_git_info(GitDir, Hash, [git(true), installed_size(Bytes)|Info]) :-
 1831    exists_directory(GitDir),
 1832    !,
 1833    git_ls_tree(Entries, [directory(GitDir)]),
 1834    git_hash(Hash, [directory(GitDir)]),
 1835    maplist(arg(4), Entries, Sizes),
 1836    sum_list(Sizes, Bytes),
 1837    dir_metadata(GitDir, Info).
 1838
 1839dir_metadata(GitDir, Info) :-
 1840    directory_file_path(GitDir, 'pack.pl', InfoFile),
 1841    read_file_to_terms(InfoFile, Info, [encoding(utf8)]),
 1842    maplist(valid_term(pack_info_term), Info).
 1843
 1844%!  download_file_sanity_check(+Archive, +Pack, +Info) is semidet.
 1845%
 1846%   Perform basic sanity checks on DownloadFile
 1847
 1848download_file_sanity_check(Archive, Pack, Info) :-
 1849    info_field(name(PackName), Info),
 1850    info_field(version(PackVersion), Info),
 1851    pack_version_file(PackFile, FileVersion, Archive),
 1852    must_match([Pack, PackName, PackFile], name),
 1853    must_match([PackVersion, FileVersion], version).
 1854
 1855info_field(Field, Info) :-
 1856    memberchk(Field, Info),
 1857    ground(Field),
 1858    !.
 1859info_field(Field, _Info) :-
 1860    functor(Field, FieldName, _),
 1861    print_message(error, pack(missing(FieldName))),
 1862    fail.
 1863
 1864must_match(Values, _Field) :-
 1865    sort(Values, [_]),
 1866    !.
 1867must_match(Values, Field) :-
 1868    print_message(error, pack(conflict(Field, Values))),
 1869    fail.
 1870
 1871
 1872                 /*******************************
 1873                 *         INSTALLATION         *
 1874                 *******************************/
 1875
 1876%!  prepare_pack_dir(+Dir, +Options)
 1877%
 1878%   Prepare for installing the package into  Dir. This
 1879%
 1880%     - If the directory exist and is empty, done.
 1881%     - Else if the directory exists, remove the directory and recreate
 1882%       it. Note that if the directory is a symlink this just deletes
 1883%       the link.
 1884%     - Else if some entry (file, link, ...) exists, delete it and
 1885%       create a new directory.
 1886%     - Else create the directory.
 1887
 1888prepare_pack_dir(Dir, Options) :-
 1889    exists_directory(Dir),
 1890    !,
 1891    (   empty_directory(Dir)
 1892    ->  true
 1893    ;   remove_existing_pack(Dir, Options)
 1894    ->  make_directory(Dir)
 1895    ).
 1896prepare_pack_dir(Dir, _) :-
 1897    (   read_link(Dir, _, _)
 1898    ;   access_file(Dir, exist)
 1899    ),
 1900    !,
 1901    delete_file(Dir),
 1902    make_directory(Dir).
 1903prepare_pack_dir(Dir, _) :-
 1904    make_directory(Dir).
 1905
 1906%!  empty_directory(+Directory) is semidet.
 1907%
 1908%   True if Directory is empty (holds no files or sub-directories).
 1909
 1910empty_directory(Dir) :-
 1911    \+ ( directory_files(Dir, Entries),
 1912         member(Entry, Entries),
 1913         \+ special(Entry)
 1914       ).
 1915
 1916special(.).
 1917special(..).
 1918
 1919%!  remove_existing_pack(+PackDir, +Options) is semidet.
 1920%
 1921%   Remove  a  possible  existing   pack    directory   if   the  option
 1922%   upgrade(true) is present. This is used to remove an old installation
 1923%   before unpacking a new archive, copy or   link  a directory with the
 1924%   new contents.
 1925
 1926remove_existing_pack(PackDir, Options) :-
 1927    exists_directory(PackDir),
 1928    !,
 1929    (   (   option(upgrade(true), Options)
 1930        ;   confirm(remove_existing_pack(PackDir), yes, Options)
 1931        )
 1932    ->  delete_directory_and_contents(PackDir)
 1933    ;   print_message(error, pack(directory_exists(PackDir))),
 1934        fail
 1935    ).
 1936remove_existing_pack(_, _).
 1937
 1938%!  pack_download_from_url(+URL, +PackDir, +Pack, +Options)
 1939%
 1940%   Download a package from a remote   source.  For git repositories, we
 1941%   simply clone. Archives are downloaded. Options:
 1942%
 1943%     - git(true)
 1944%       Assume URL refers to a git repository.
 1945%     - pack_dir(-Dir)
 1946%       Dir is unified with the location where the pack is installed.
 1947%
 1948%   @tbd We currently  use  the  built-in   HTTP  client.  For  complete
 1949%   coverage, we should consider using  an   external  (e.g., `curl`) if
 1950%   available.
 1951
 1952pack_download_from_url(URL, PackTopDir, Pack, Options) :-
 1953    option(git(true), Options),
 1954    !,
 1955    directory_file_path(PackTopDir, Pack, PackDir),
 1956    prepare_pack_dir(PackDir, Options),
 1957    (   option(branch(Branch), Options)
 1958    ->  Extra = ['--branch', Branch]
 1959    ;   Extra = []
 1960    ),
 1961    run_process(path(git), [clone, URL, PackDir|Extra], []),
 1962    git_checkout_version(PackDir, [update(false)|Options]),
 1963    option(pack_dir(PackDir), Options, _).
 1964pack_download_from_url(URL0, PackTopDir, Pack, Options) :-
 1965    download_url(URL0),
 1966    !,
 1967    hsts(URL0, URL, Options),
 1968    directory_file_path(PackTopDir, Pack, PackDir),
 1969    prepare_pack_dir(PackDir, Options),
 1970    pack_download_dir(PackTopDir, DownLoadDir),
 1971    download_file(URL, Pack, DownloadBase, Options),
 1972    directory_file_path(DownLoadDir, DownloadBase, DownloadFile),
 1973    (   option(insecure(true), Options, false)
 1974    ->  TLSOptions = [cert_verify_hook(ssl_verify)]
 1975    ;   TLSOptions = []
 1976    ),
 1977    print_message(informational, pack(download(begin, Pack, URL, DownloadFile))),
 1978    setup_call_cleanup(
 1979        http_open(URL, In, TLSOptions),
 1980        setup_call_cleanup(
 1981            open(DownloadFile, write, Out, [type(binary)]),
 1982            copy_stream_data(In, Out),
 1983            close(Out)),
 1984        close(In)),
 1985    print_message(informational, pack(download(end, Pack, URL, DownloadFile))),
 1986    pack_archive_info(DownloadFile, Pack, Info, _),
 1987    (   option(git_url(GitURL), Options)
 1988    ->  Origin = GitURL                 % implicit download from git.
 1989    ;   download_file_sanity_check(DownloadFile, Pack, Info),
 1990        Origin = URL
 1991    ),
 1992    pack_unpack_from_local(DownloadFile, PackTopDir, Pack, PackDir, Options),
 1993    pack_assert(PackDir, archive(DownloadFile, Origin)),
 1994    option(pack_dir(PackDir), Options, _).
 1995pack_download_from_url(URL, PackTopDir, Pack, Options) :-
 1996    local_uri_file_name(URL, File),
 1997    !,
 1998    pack_unpack_from_local(File, PackTopDir, Pack, PackDir, Options),
 1999    pack_assert(PackDir, archive(File, URL)),
 2000    option(pack_dir(PackDir), Options, _).
 2001pack_download_from_url(URL, _PackTopDir, _Pack, _Options) :-
 2002    domain_error(url, URL).
 2003
 2004%!  git_checkout_version(+PackDir, +Options) is det.
 2005%
 2006%   Given a checked out version of a repository, put the repo at the
 2007%   desired version.  Options:
 2008%
 2009%     - commit(+Commit)
 2010%       Target commit or `'HEAD'`.  If `'HEAD'`, get the HEAD of the
 2011%       explicit (option branch(Branch)), current or default branch. If
 2012%       the commit is a hash and it is the tip of a branch, checkout
 2013%       this branch. Else simply checkout the hash.
 2014%     - branch(+Branch)
 2015%       Used with commit('HEAD').
 2016%     - version(+Version)
 2017%       Checkout a tag.  If there is a tag matching Version use that,
 2018%       otherwise try to find a tag that ends with Version and demand
 2019%       the prefix to be letters, optionally followed by a dash or
 2020%       underscore.  Examples: 2.1, V2.1, v_2.1.
 2021%     - update(true)
 2022%       If none of the above is given update the repo.  If it is on
 2023%       a branch, _pull_.  Else, put it on the default branch and
 2024%       pull.
 2025
 2026git_checkout_version(PackDir, Options) :-
 2027    option(commit('HEAD'), Options),
 2028    option(branch(Branch), Options),
 2029    !,
 2030    git_ensure_on_branch(PackDir, Branch),
 2031    run_process(path(git), ['-C', PackDir, pull], []).
 2032git_checkout_version(PackDir, Options) :-
 2033    option(commit('HEAD'), Options),
 2034    git_current_branch(_, [directory(PackDir)]),
 2035    !,
 2036    run_process(path(git), ['-C', PackDir, pull], []).
 2037git_checkout_version(PackDir, Options) :-
 2038    option(commit('HEAD'), Options),
 2039    !,
 2040    git_default_branch(Branch, [directory(PackDir)]),
 2041    git_ensure_on_branch(PackDir, Branch),
 2042    run_process(path(git), ['-C', PackDir, pull], []).
 2043git_checkout_version(PackDir, Options) :-
 2044    option(commit(Hash), Options),
 2045    run_process(path(git), ['-C', PackDir, fetch], []),
 2046    git_branches(Branches, [contains(Hash), directory(PackDir)]),
 2047    git_process_output(['-C', PackDir, 'rev-parse' | Branches],
 2048                       read_lines_to_atoms(Commits),
 2049                       []),
 2050    nth1(I, Commits, Hash),
 2051    nth1(I, Branches, Branch),
 2052    !,
 2053    git_ensure_on_branch(PackDir, Branch).
 2054git_checkout_version(PackDir, Options) :-
 2055    option(commit(Hash), Options),
 2056    !,
 2057    run_process(path(git), ['-C', PackDir, checkout, '--quiet', Hash], []).
 2058git_checkout_version(PackDir, Options) :-
 2059    option(version(Version), Options),
 2060    !,
 2061    git_tags(Tags, [directory(PackDir)]),
 2062    (   memberchk(Version, Tags)
 2063    ->  Tag = Version
 2064    ;   member(Tag, Tags),
 2065        sub_atom(Tag, B, _, 0, Version),
 2066        sub_atom(Tag, 0, B, _, Prefix),
 2067        version_prefix(Prefix)
 2068    ->  true
 2069    ;   existence_error(version_tag, Version)
 2070    ),
 2071    run_process(path(git), ['-C', PackDir, checkout, Tag], []).
 2072git_checkout_version(_PackDir, Options) :-
 2073    option(fresh(true), Options),
 2074    !.
 2075git_checkout_version(PackDir, _Options) :-
 2076    git_current_branch(_, [directory(PackDir)]),
 2077    !,
 2078    run_process(path(git), ['-C', PackDir, pull], []).
 2079git_checkout_version(PackDir, _Options) :-
 2080    git_default_branch(Branch, [directory(PackDir)]),
 2081    git_ensure_on_branch(PackDir, Branch),
 2082    run_process(path(git), ['-C', PackDir, pull], []).
 2083
 2084%!  git_ensure_on_branch(+PackDir, +Branch) is det.
 2085%
 2086%   Ensure PackDir is on Branch.
 2087
 2088git_ensure_on_branch(PackDir, Branch) :-
 2089    git_current_branch(Branch, [directory(PackDir)]),
 2090    !.
 2091git_ensure_on_branch(PackDir, Branch) :-
 2092    run_process(path(git), ['-C', PackDir, checkout, Branch], []).
 2093
 2094read_lines_to_atoms(Atoms, In) :-
 2095    read_line_to_string(In, Line),
 2096    (   Line == end_of_file
 2097    ->  Atoms = []
 2098    ;   atom_string(Atom, Line),
 2099        Atoms = [Atom|T],
 2100        read_lines_to_atoms(T, In)
 2101    ).
 2102
 2103version_prefix(Prefix) :-
 2104    atom_codes(Prefix, Codes),
 2105    phrase(version_prefix, Codes).
 2106
 2107version_prefix -->
 2108    [C],
 2109    { code_type(C, alpha) },
 2110    !,
 2111    version_prefix.
 2112version_prefix -->
 2113    "-".
 2114version_prefix -->
 2115    "_".
 2116version_prefix -->
 2117    "".
 2118
 2119%!  download_file(+URL, +Pack, -File, +Options) is det.
 2120%
 2121%   Determine the file into which  to   download  URL. The second clause
 2122%   deals with GitHub downloads from a release tag.
 2123
 2124download_file(URL, Pack, File, Options) :-
 2125    option(version(Version), Options),
 2126    !,
 2127    file_name_extension(_, Ext, URL),
 2128    format(atom(File), '~w-~w.~w', [Pack, Version, Ext]).
 2129download_file(URL, Pack, File, _) :-
 2130    file_base_name(URL,Basename),
 2131    no_int_file_name_extension(Tag,Ext,Basename),
 2132    tag_version(Tag,Version),
 2133    !,
 2134    format(atom(File0), '~w-~w', [Pack, Version]),
 2135    file_name_extension(File0, Ext, File).
 2136download_file(URL, _, File, _) :-
 2137    file_base_name(URL, File).
 2138
 2139%!  pack_url_file(+URL, -File) is det.
 2140%
 2141%   True if File is a unique  id   for  the referenced pack and version.
 2142%   Normally, that is simply the base  name, but GitHub archives destroy
 2143%   this picture. Needed by the pack manager in the web server.
 2144
 2145:- public pack_url_file/2. 2146pack_url_file(URL, FileID) :-
 2147    github_release_url(URL, Pack, Version),
 2148    !,
 2149    download_file(URL, Pack, FileID, [version(Version)]).
 2150pack_url_file(URL, FileID) :-
 2151    file_base_name(URL, FileID).
 2152
 2153%   ssl_verify(+SSL, +ProblemCert, +AllCerts, +FirstCert, +Error)
 2154%
 2155%   Used if insecure(true)  is  given   to  pack_install/2.  Accepts any
 2156%   certificate.
 2157
 2158:- public ssl_verify/5. 2159ssl_verify(_SSL,
 2160           _ProblemCertificate, _AllCertificates, _FirstCertificate,
 2161           _Error).
 2162
 2163pack_download_dir(PackTopDir, DownLoadDir) :-
 2164    directory_file_path(PackTopDir, 'Downloads', DownLoadDir),
 2165    (   exists_directory(DownLoadDir)
 2166    ->  true
 2167    ;   make_directory(DownLoadDir)
 2168    ),
 2169    (   access_file(DownLoadDir, write)
 2170    ->  true
 2171    ;   permission_error(write, directory, DownLoadDir)
 2172    ).
 2173
 2174%!  download_url(@URL) is semidet.
 2175%
 2176%   True if URL looks like a URL we   can  download from. Noet that urls
 2177%   like ``ftp://`` are also download  URLs,   but  _we_ cannot download
 2178%   from them.
 2179
 2180download_url(URL) :-
 2181    url_scheme(URL, Scheme),
 2182    download_scheme(Scheme).
 2183
 2184url_scheme(URL, Scheme) :-
 2185    atom(URL),
 2186    uri_components(URL, Components),
 2187    uri_data(scheme, Components, Scheme0),
 2188    atom(Scheme0),
 2189    Scheme = Scheme0.
 2190
 2191download_scheme(http).
 2192download_scheme(https).
 2193
 2194%!  hsts(+URL0, -URL, +Options) is det.
 2195%
 2196%   HSTS (HTTP Strict Transport Security) is   standard by which means a
 2197%   site asks to always use HTTPS. For  SWI-Prolog packages we now force
 2198%   using HTTPS for all  downloads.  This   may  be  overrules using the
 2199%   option insecure(true), which  may  also  be   used  to  disable  TLS
 2200%   certificate  checking.  Note  that  the   pack  integrity  is  still
 2201%   protected by its SHA1 hash.
 2202
 2203hsts(URL0, URL, Options) :-
 2204    option(insecure(true), Options, false),
 2205    !,
 2206    URL = URL0.
 2207hsts(URL0, URL, _Options) :-
 2208    url_scheme(URL0, http),
 2209    !,
 2210    uri_edit(scheme(https), URL0, URL).
 2211hsts(URL, URL, _Options).
 2212
 2213
 2214%!  pack_post_install(+Info, +Options) is det.
 2215%
 2216%   Process post installation work.  Steps:
 2217%
 2218%     - Create foreign resources
 2219%     - Register directory as autoload library
 2220%     - Attach the package
 2221
 2222pack_post_install(Info, Options) :-
 2223    Pack = Info.pack,
 2224    PackDir = Info.installed,
 2225    post_install_foreign(Pack, PackDir, Options),
 2226    post_install_autoload(Info),
 2227    pack_attach(PackDir, [duplicate(warning)]).
 2228
 2229%!  pack_rebuild is det.
 2230%!  pack_rebuild(+Pack) is det.
 2231%
 2232%   Rebuild  possible  foreign  components  of    Pack.   The  predicate
 2233%   pack_rebuild/0 rebuilds all registered packs.
 2234
 2235pack_rebuild :-
 2236    forall(current_pack(Pack),
 2237           ( print_message(informational, pack(rebuild(Pack))),
 2238             pack_rebuild(Pack)
 2239           )).
 2240
 2241pack_rebuild(Pack) :-
 2242    current_pack(Pack, PackDir),
 2243    !,
 2244    post_install_foreign(Pack, PackDir, [rebuild(true)]),
 2245	pack_attach(PackDir, [duplicate(replace)]).
 2246pack_rebuild(Pack) :-
 2247    unattached_pack(Pack, PackDir),
 2248    !,
 2249    post_install_foreign(Pack, PackDir, [rebuild(true)]),
 2250	pack_attach(PackDir, [duplicate(replace)]).
 2251pack_rebuild(Pack) :-
 2252    existence_error(pack, Pack).
 2253
 2254unattached_pack(Pack, BaseDir) :-
 2255    directory_file_path(Pack, 'pack.pl', PackFile),
 2256    absolute_file_name(pack(PackFile), PackPath,
 2257                       [ access(read),
 2258                         file_errors(fail)
 2259                       ]),
 2260    file_directory_name(PackPath, BaseDir).
 2261
 2262
 2263
 2264%!  post_install_foreign(+Pack, +PackDir, +Options) is det.
 2265%
 2266%   Install foreign parts of the package.  Options:
 2267%
 2268%     - rebuild(When)
 2269%       Determine when to rebuild.  Possible values:
 2270%       - if_absent
 2271%         Only rebuild if we have no existing foreign library.  This
 2272%         is the default.
 2273%       - true
 2274%         Always rebuild.
 2275
 2276post_install_foreign(Pack, PackDir, Options) :-
 2277    is_foreign_pack(PackDir, _),
 2278    !,
 2279    (   pack_info_term(PackDir, pack_version(Version))
 2280    ->  true
 2281    ;   Version = 1
 2282    ),
 2283    option(rebuild(Rebuild), Options, if_absent),
 2284    current_prolog_flag(arch, Arch),
 2285    prolog_version_dotted(PrologVersion),
 2286    (   Rebuild == if_absent,
 2287        foreign_present(PackDir, Arch)
 2288    ->  print_message(informational, pack(kept_foreign(Pack, Arch))),
 2289        (   pack_status_dir(PackDir, built(Arch, _, _))
 2290        ->  true
 2291        ;   pack_assert(PackDir, built(Arch, PrologVersion, downloaded))
 2292        )
 2293    ;   BuildSteps0 = [[dependencies], [configure], build, install, [test]],
 2294        (   Rebuild == true
 2295        ->  BuildSteps1 = [distclean|BuildSteps0]
 2296        ;   BuildSteps1 = BuildSteps0
 2297        ),
 2298        (   option(test(false), Options)
 2299        ->  delete(BuildSteps1, [test], BuildSteps2)
 2300        ;   BuildSteps2 = BuildSteps1
 2301        ),
 2302        (   option(clean(true), Options)
 2303        ->  append(BuildSteps2, [[clean]], BuildSteps)
 2304        ;   BuildSteps = BuildSteps2
 2305        ),
 2306        build_steps(BuildSteps, PackDir, [pack_version(Version)|Options]),
 2307        pack_assert(PackDir, built(Arch, PrologVersion, built))
 2308    ).
 2309post_install_foreign(_, _, _).
 2310
 2311
 2312%!  foreign_present(+PackDir, +Arch) is semidet.
 2313%
 2314%   True if we find one or more modules  in the pack `lib` directory for
 2315%   the current architecture.
 2316%
 2317%   @tbd Does not check that  these  can   be  loaded,  nor  whether all
 2318%   required modules are present.
 2319
 2320foreign_present(PackDir, Arch) :-
 2321    atomic_list_concat([PackDir, '/lib'], ForeignBaseDir),
 2322    exists_directory(ForeignBaseDir),
 2323    !,
 2324    atomic_list_concat([PackDir, '/lib/', Arch], ForeignDir),
 2325    exists_directory(ForeignDir),
 2326    current_prolog_flag(shared_object_extension, Ext),
 2327    atomic_list_concat([ForeignDir, '/*.', Ext], Pattern),
 2328    expand_file_name(Pattern, Files),
 2329    Files \== [].
 2330
 2331%!  is_foreign_pack(+PackDir, -Type) is nondet.
 2332%
 2333%   True when PackDir contains  files  that   indicate  the  need  for a
 2334%   specific class of build tools indicated by Type.
 2335
 2336is_foreign_pack(PackDir, Type) :-
 2337    foreign_file(File, Type),
 2338    directory_file_path(PackDir, File, Path),
 2339    exists_file(Path).
 2340
 2341foreign_file('CMakeLists.txt', cmake).
 2342foreign_file('configure',      configure).
 2343foreign_file('configure.in',   autoconf).
 2344foreign_file('configure.ac',   autoconf).
 2345foreign_file('Makefile.am',    automake).
 2346foreign_file('Makefile',       make).
 2347foreign_file('makefile',       make).
 2348foreign_file('conanfile.txt',  conan).
 2349foreign_file('conanfile.py',   conan).
 2350
 2351
 2352                 /*******************************
 2353                 *           AUTOLOAD           *
 2354                 *******************************/
 2355
 2356%!  post_install_autoload(+InfoOrList) is det.
 2357%
 2358%   Create an autoload index if the package demands such.
 2359
 2360post_install_autoload(List), is_list(List) =>
 2361    maplist(post_install_autoload, List).
 2362post_install_autoload(Info),
 2363    _{installed:PackDir, autoload:true} :< Info =>
 2364    directory_file_path(PackDir, prolog, PrologLibDir),
 2365    make_library_index(PrologLibDir).
 2366post_install_autoload(Info) =>
 2367    directory_file_path(Info.installed, 'prolog/INDEX.pl', IndexFile),
 2368    (   exists_file(IndexFile)
 2369    ->  E = error(_,_),
 2370        print_message(warning, pack(delete_autoload_index(Info.pack, IndexFile))),
 2371        catch(delete_file(IndexFile), E,
 2372              print_message(warning, E))
 2373    ;   true
 2374    ).
 2375
 2376%!  decide_autoload_pack(+Options, +Info0, -Info) is det.
 2377%
 2378%   Add autoload:true to Info if the  pack   needs  to be configured for
 2379%   autoloading.
 2380
 2381decide_autoload_pack(Options, Info0, Info) :-
 2382    is_autoload_pack(Info0.pack, Info0.installed, Options),
 2383    !,
 2384    Info = Info0.put(autoload, true).
 2385decide_autoload_pack(_, Info, Info).
 2386
 2387is_autoload_pack(_Pack, _PackDir, Options) :-
 2388    option(autoload(true), Options),
 2389    !.
 2390is_autoload_pack(Pack, PackDir, Options) :-
 2391    pack_info_term(PackDir, autoload(true)),
 2392    confirm(autoload(Pack), no, Options).
 2393
 2394
 2395                 /*******************************
 2396                 *            UPGRADE           *
 2397                 *******************************/
 2398
 2399%!  pack_upgrade(+Pack) is semidet.
 2400%
 2401%   Upgrade Pack.  Shorthand for pack_install(Pack, [upgrade(true)]).
 2402
 2403pack_upgrade(Pack) :-
 2404    pack_install(Pack, [upgrade(true)]).
 2405
 2406
 2407                 /*******************************
 2408                 *            REMOVE            *
 2409                 *******************************/
 2410
 2411%!  pack_remove(+Name) is det.
 2412%!  pack_remove(+Name, +Options) is det.
 2413%
 2414%   Remove the indicated package.  If   packages  depend (indirectly) on
 2415%   this pack, ask to remove these as well.  Options:
 2416%
 2417%     - interactive(false)
 2418%       Do not prompt the user.
 2419%     - dependencies(Boolean)
 2420%       If `true` delete dependencies without asking.
 2421
 2422pack_remove(Pack) :-
 2423    pack_remove(Pack, []).
 2424
 2425pack_remove(Pack, Options) :-
 2426    option(dependencies(false), Options),
 2427    !,
 2428    pack_remove_forced(Pack).
 2429pack_remove(Pack, Options) :-
 2430    (   dependents(Pack, Deps)
 2431    ->  (   option(dependencies(true), Options)
 2432        ->  true
 2433        ;   confirm_remove(Pack, Deps, Delete, Options)
 2434        ),
 2435        forall(member(P, Delete), pack_remove_forced(P))
 2436    ;   pack_remove_forced(Pack)
 2437    ).
 2438
 2439pack_remove_forced(Pack) :-
 2440    catch('$pack_detach'(Pack, BaseDir),
 2441          error(existence_error(pack, Pack), _),
 2442          fail),
 2443    !,
 2444    (   read_link(BaseDir, _, Target)
 2445    ->  What = link(Target)
 2446    ;   What = directory
 2447    ),
 2448    print_message(informational, pack(remove(What, BaseDir))),
 2449    delete_directory_and_contents(BaseDir).
 2450pack_remove_forced(Pack) :-
 2451    unattached_pack(Pack, BaseDir),
 2452    !,
 2453    delete_directory_and_contents(BaseDir).
 2454pack_remove_forced(Pack) :-
 2455    print_message(informational, error(existence_error(pack, Pack),_)).
 2456
 2457confirm_remove(Pack, Deps, Delete, Options) :-
 2458    print_message(warning, pack(depends(Pack, Deps))),
 2459    menu(pack(resolve_remove),
 2460         [ [Pack]      = remove_only(Pack),
 2461           [Pack|Deps] = remove_deps(Pack, Deps),
 2462           []          = cancel
 2463         ], [], Delete, Options),
 2464    Delete \== [].
 2465
 2466
 2467		 /*******************************
 2468		 *           PUBLISH		*
 2469		 *******************************/
 2470
 2471%!  pack_publish(+Spec, +Options) is det.
 2472%
 2473%   Publish a package. There are two ways  typical ways to call this. We
 2474%   recommend developing a pack in a   GIT  repository. In this scenario
 2475%   the pack can be published using
 2476%
 2477%       ?- pack_publish('.', []).
 2478%
 2479%   Alternatively, an archive  file  has  been   uploaded  to  a  public
 2480%   location. In this scenario we can publish the pack using
 2481%
 2482%       ?- pack_publish(URL, [])
 2483%
 2484%   In both scenarios, pack_publish/2  by   default  creates an isolated
 2485%   environment and installs the package  in   this  directory  from the
 2486%   public URL. On success it triggers the   pack server to register the
 2487%   URL as a new pack or a new release of a pack.
 2488%
 2489%   Packs may also be published using the _app_ `pack`, e.g.
 2490%
 2491%       swipl pack publish .
 2492%
 2493%   Options:
 2494%
 2495%     - git(Boolean)
 2496%       If `true`, and Spec is a git managed directory, install using
 2497%       the remote repo.
 2498%     - sign(Boolean)
 2499%       Sign the repository with the current version.  This runs
 2500%       ``git tag -s <tag>``.
 2501%     - force(Boolean)
 2502%       Force the git tag.  This runs ``git tag -f <tag>``.
 2503%     - branch(+Branch)
 2504%       Branch used for releases.  Defined by git_default_branch/2
 2505%       if not specified.
 2506%     - register(+Boolean)
 2507%       If `false` (default `true`), perform the installation, but do
 2508%       not upload to the server. This can be used for testing.
 2509%     - isolated(+Boolean)
 2510%       If `true` (default), install and build all packages in an
 2511%       isolated package directory.  If `false`, use other packages
 2512%       installed for the environment.   The latter may be used to
 2513%       speedup debugging.
 2514%     - pack_directory(+Dir)
 2515%       Install the temporary packages in Dir. If omitted pack_publish/2
 2516%       creates a temporary directory and deletes this directory after
 2517%       completion. An explict target Dir is created if it does not
 2518%       exist and is not deleted on completion.
 2519%     - clean(+Boolean)
 2520%       If `true` (default), clean the destination directory first
 2521
 2522pack_publish(Dir, Options) :-
 2523    \+ download_url(Dir),
 2524    is_git_directory(Dir), !,
 2525    pack_git_info(Dir, _Hash, Metadata),
 2526    prepare_repository(Dir, Metadata, Options),
 2527    (   memberchk(download(URL), Metadata),
 2528        git_url(URL, _)
 2529    ->  true
 2530    ;   option(remote(Remote), Options, origin),
 2531        git_remote_url(Remote, RemoteURL, [directory(Dir)]),
 2532        git_to_https_url(RemoteURL, URL)
 2533    ),
 2534    memberchk(version(Version), Metadata),
 2535    pack_publish_(URL,
 2536                  [ version(Version)
 2537                  | Options
 2538                  ]).
 2539pack_publish(Spec, Options) :-
 2540    pack_publish_(Spec, Options).
 2541
 2542pack_publish_(Spec, Options) :-
 2543    pack_default_options(Spec, Pack, Options, DefOptions),
 2544    option(url(URL), DefOptions),
 2545    valid_publish_url(URL, Options),
 2546    prepare_build_location(Pack, Dir, Clean, Options),
 2547    (   option(register(false), Options)
 2548    ->  InstallOptions = DefOptions
 2549    ;   InstallOptions = [publish(Pack)|DefOptions]
 2550    ),
 2551    call_cleanup(pack_install(Pack,
 2552                              [ pack(Pack)
 2553                              | InstallOptions
 2554                              ]),
 2555                 cleanup_publish(Clean, Dir)).
 2556
 2557cleanup_publish(true, Dir) :-
 2558    !,
 2559    delete_directory_and_contents(Dir).
 2560cleanup_publish(_, _).
 2561
 2562valid_publish_url(URL, Options) :-
 2563    option(register(Register), Options, true),
 2564    (   Register == false
 2565    ->  true
 2566    ;   download_url(URL)
 2567    ->  true
 2568    ;   permission_error(publish, pack, URL)
 2569    ).
 2570
 2571prepare_build_location(Pack, Dir, Clean, Options) :-
 2572    (   option(pack_directory(Dir), Options)
 2573    ->  ensure_directory(Dir),
 2574        (   option(clean(true), Options, true)
 2575        ->  delete_directory_contents(Dir)
 2576        ;   true
 2577        )
 2578    ;   tmp_file(pack, Dir),
 2579        make_directory(Dir),
 2580        Clean = true
 2581    ),
 2582    (   option(isolated(false), Options)
 2583    ->  detach_pack(Pack, _),
 2584        attach_packs(Dir, [search(first)])
 2585    ;   attach_packs(Dir, [replace(true)])
 2586    ).
 2587
 2588
 2589
 2590%!  prepare_repository(+Dir, +Metadata, +Options) is semidet.
 2591%
 2592%   Prepare the git repository. If register(false)  is provided, this is
 2593%   a test run and therefore we do   not  need this. Otherwise we demand
 2594%   the working directory to be clean,  we   tag  the current commit and
 2595%   push the current branch.
 2596
 2597prepare_repository(_Dir, _Metadata, Options) :-
 2598    option(register(false), Options),
 2599    !.
 2600prepare_repository(Dir, Metadata, Options) :-
 2601    git_dir_must_be_clean(Dir),
 2602    git_must_be_on_default_branch(Dir, Options),
 2603    tag_git_dir(Dir, Metadata, Action, Options),
 2604    confirm(git_push, yes, Options),
 2605    run_process(path(git), ['-C', file(Dir), push ], []),
 2606    (   Action = push_tag(Tag)
 2607    ->  run_process(path(git), ['-C', file(Dir), push, origin, Tag ], [])
 2608    ;   true
 2609    ).
 2610
 2611git_dir_must_be_clean(Dir) :-
 2612    git_describe(Description, [directory(Dir)]),
 2613    (   sub_atom(Description, _, _, 0, '-DIRTY')
 2614    ->  print_message(error, pack(git_not_clean(Dir))),
 2615        fail
 2616    ;   true
 2617    ).
 2618
 2619git_must_be_on_default_branch(Dir, Options) :-
 2620    (   option(branch(Default), Options)
 2621    ->  true
 2622    ;   git_default_branch(Default, [directory(Dir)])
 2623    ),
 2624    git_current_branch(Current, [directory(Dir)]),
 2625    (   Default == Current
 2626    ->  true
 2627    ;   print_message(error,
 2628                      pack(git_branch_not_default(Dir, Default, Current))),
 2629        fail
 2630    ).
 2631
 2632
 2633%!  tag_git_dir(+Dir, +Metadata, -Action, +Options) is semidet.
 2634%
 2635%   Add a version tag to the git repository.
 2636%
 2637%   @arg Action is one of push_tag(Tag) or `none`
 2638
 2639tag_git_dir(Dir, Metadata, Action, Options) :-
 2640    memberchk(version(Version), Metadata),
 2641    atom_concat('V', Version, Tag),
 2642    git_tags(Tags, [directory(Dir)]),
 2643    (   memberchk(Tag, Tags)
 2644    ->  git_tag_is_consistent(Dir, Tag, Action, Options)
 2645    ;   format(string(Message), 'Release ~w', [Version]),
 2646        findall(Opt, git_tag_option(Opt, Options), Argv,
 2647                [ '-m', Message, Tag ]),
 2648        confirm(git_tag(Tag), yes, Options),
 2649        run_process(path(git), ['-C', file(Dir), tag | Argv ], []),
 2650        Action = push_tag(Tag)
 2651    ).
 2652
 2653git_tag_option('-s', Options) :- option(sign(true), Options, true).
 2654git_tag_option('-f', Options) :- option(force(true), Options, true).
 2655
 2656git_tag_is_consistent(Dir, Tag, Action, Options) :-
 2657    format(atom(TagRef), 'refs/tags/~w', [Tag]),
 2658    format(atom(CommitRef), 'refs/tags/~w^{}', [Tag]),
 2659    option(remote(Remote), Options, origin),
 2660    git_ls_remote(Dir, LocalTags, [tags(true)]),
 2661    memberchk(CommitHash-CommitRef, LocalTags),
 2662    (   git_hash(CommitHash, [directory(Dir)])
 2663    ->  true
 2664    ;   print_message(error, pack(git_release_tag_not_at_head(Tag))),
 2665        fail
 2666    ),
 2667    memberchk(TagHash-TagRef, LocalTags),
 2668    git_ls_remote(Remote, RemoteTags, [tags(true)]),
 2669    (   memberchk(RemoteCommitHash-CommitRef, RemoteTags),
 2670        memberchk(RemoteTagHash-TagRef, RemoteTags)
 2671    ->  (   RemoteCommitHash == CommitHash,
 2672            RemoteTagHash == TagHash
 2673        ->  Action = none
 2674        ;   print_message(error, pack(git_tag_out_of_sync(Tag))),
 2675            fail
 2676        )
 2677    ;   Action = push_tag(Tag)
 2678    ).
 2679
 2680%!  git_to_https_url(+GitURL, -HTTP_URL) is semidet.
 2681%
 2682%   Get the HTTP(s) URL for a git repository, given a git url.
 2683%   Whether or not this is available and how to translate the
 2684%   one into the other depends in the server software.
 2685
 2686git_to_https_url(URL, URL) :-
 2687    download_url(URL),
 2688    !.
 2689git_to_https_url(GitURL, URL) :-
 2690    atom_concat('git@github.com:', Repo, GitURL),
 2691    !,
 2692    atom_concat('https://github.com/', Repo, URL).
 2693git_to_https_url(GitURL, _) :-
 2694    print_message(error, pack(git_no_https(GitURL))),
 2695    fail.
 2696
 2697
 2698                 /*******************************
 2699                 *           PROPERTIES         *
 2700                 *******************************/
 2701
 2702%!  pack_property(?Pack, ?Property) is nondet.
 2703%
 2704%   True when Property  is  a  property   of  an  installed  Pack.  This
 2705%   interface is intended for programs that   wish  to interact with the
 2706%   package manager. Defined properties are:
 2707%
 2708%     - directory(Directory)
 2709%     Directory into which the package is installed
 2710%     - version(Version)
 2711%     Installed version
 2712%     - title(Title)
 2713%     Full title of the package
 2714%     - author(Author)
 2715%     Registered author
 2716%     - download(URL)
 2717%     Official download URL
 2718%     - readme(File)
 2719%     Package README file (if present)
 2720%     - todo(File)
 2721%     Package TODO file (if present)
 2722
 2723pack_property(Pack, Property) :-
 2724    findall(Pack-Property, pack_property_(Pack, Property), List),
 2725    member(Pack-Property, List).            % make det if applicable
 2726
 2727pack_property_(Pack, Property) :-
 2728    pack_info(Pack, _, Property).
 2729pack_property_(Pack, Property) :-
 2730    \+ \+ info_file(Property, _),
 2731    '$pack':pack(Pack, BaseDir),
 2732    access_file(BaseDir, read),
 2733    directory_files(BaseDir, Files),
 2734    member(File, Files),
 2735    info_file(Property, Pattern),
 2736    downcase_atom(File, Pattern),
 2737    directory_file_path(BaseDir, File, InfoFile),
 2738    arg(1, Property, InfoFile).
 2739
 2740info_file(readme(_), 'readme.txt').
 2741info_file(readme(_), 'readme').
 2742info_file(todo(_),   'todo.txt').
 2743info_file(todo(_),   'todo').
 2744
 2745
 2746                 /*******************************
 2747                 *         VERSION LOGIC        *
 2748                 *******************************/
 2749
 2750%!  pack_version_file(-Pack, -Version:atom, +File) is semidet.
 2751%
 2752%   True if File is the  name  of  a   file  or  URL  of a file that
 2753%   contains Pack at Version. File must   have  an extension and the
 2754%   basename  must  be  of   the    form   <pack>-<n>{.<m>}*.  E.g.,
 2755%   =|mypack-1.5|=.
 2756
 2757pack_version_file(Pack, Version, GitHubRelease) :-
 2758    atomic(GitHubRelease),
 2759    github_release_url(GitHubRelease, Pack, Version),
 2760    !.
 2761pack_version_file(Pack, Version, Path) :-
 2762    atomic(Path),
 2763    file_base_name(Path, File),
 2764    no_int_file_name_extension(Base, _Ext, File),
 2765    atom_codes(Base, Codes),
 2766    (   phrase(pack_version(Pack, Version), Codes),
 2767        safe_pack_name(Pack)
 2768    ->  true
 2769    ).
 2770
 2771no_int_file_name_extension(Base, Ext, File) :-
 2772    file_name_extension(Base0, Ext0, File),
 2773    \+ atom_number(Ext0, _),
 2774    !,
 2775    Base = Base0,
 2776    Ext = Ext0.
 2777no_int_file_name_extension(File, '', File).
 2778
 2779%!  safe_pack_name(+Name:atom) is semidet.
 2780%
 2781%   Verifies that Name is a valid   pack  name. This avoids trickery
 2782%   with pack file names to make shell commands behave unexpectly.
 2783
 2784safe_pack_name(Name) :-
 2785    atom_length(Name, Len),
 2786    Len >= 3,                               % demand at least three length
 2787    atom_codes(Name, Codes),
 2788    maplist(safe_pack_char, Codes),
 2789    !.
 2790
 2791safe_pack_char(C) :- between(0'a, 0'z, C), !.
 2792safe_pack_char(C) :- between(0'A, 0'Z, C), !.
 2793safe_pack_char(C) :- between(0'0, 0'9, C), !.
 2794safe_pack_char(0'_).
 2795
 2796%!  pack_version(-Pack:atom, -Version:atom)// is semidet.
 2797%
 2798%   True when the input statifies <pack>-<version>
 2799
 2800pack_version(Pack, Version) -->
 2801    string(Codes), "-",
 2802    version(Parts),
 2803    !,
 2804    { atom_codes(Pack, Codes),
 2805      atomic_list_concat(Parts, '.', Version)
 2806    }.
 2807
 2808version([H|T]) -->
 2809    version_part(H),
 2810    (   "."
 2811    ->  version(T)
 2812    ;   {T=[]}
 2813    ).
 2814
 2815version_part(*) --> "*", !.
 2816version_part(Int) --> integer(Int).
 2817
 2818
 2819		 /*******************************
 2820		 *           GIT LOGIC		*
 2821		 *******************************/
 2822
 2823%!  have_git is semidet.
 2824%
 2825%   True if we have the `git` program.   This could be simple, but Apple
 2826%   decided to include a fake  `/usr/bin/git`   that  triggers the Xcode
 2827%   installation. So, if we find `git` at `/usr/bin/git` we should check
 2828%   that Xcode is properly enabled. This   is the case if ``xcode-select
 2829%   -p`` points at an Xcode installation. Note  that if we find `git` at
 2830%   some other location,  we  assume  it   is  installed  by  the  user,
 2831%   Macports, Homebrew or something else.
 2832
 2833have_git :-
 2834    process_which(path(git), GIT),
 2835    is_sane_git(GIT).
 2836
 2837:- if(current_prolog_flag(apple, true)). 2838sane_xcode_path -->
 2839    "Xcode.app/Contents".
 2840sane_xcode_path -->
 2841    "CommandLineTools".
 2842
 2843is_sane_git('/usr/bin/git') :-
 2844    !,
 2845    process_which(path('xcode-select'), XSpath),
 2846    catch(run_process(XSpath,['-p'],[output(Output),error(_)]), error(_,_), fail),
 2847    once(phrase((string(_), sane_xcode_path), Output, _)).
 2848:- endif. 2849is_sane_git(_).
 2850
 2851%!  git_url(+URL, -Pack) is semidet.
 2852%
 2853%   True if URL describes a git url for Pack
 2854
 2855git_url(URL, Pack) :-
 2856    uri_components(URL, Components),
 2857    uri_data(scheme, Components, Scheme),
 2858    nonvar(Scheme),                         % must be full URL
 2859    uri_data(path, Components, Path),
 2860    (   Scheme == git
 2861    ->  true
 2862    ;   git_download_scheme(Scheme),
 2863        file_name_extension(_, git, Path)
 2864    ;   git_download_scheme(Scheme),
 2865        catch(git_ls_remote(URL, _, [refs(['HEAD']), error(_)]), _, fail)
 2866    ->  true
 2867    ),
 2868    file_base_name(Path, PackExt),
 2869    (   file_name_extension(Pack, git, PackExt)
 2870    ->  true
 2871    ;   Pack = PackExt
 2872    ),
 2873    (   safe_pack_name(Pack)
 2874    ->  true
 2875    ;   domain_error(pack_name, Pack)
 2876    ).
 2877
 2878git_download_scheme(http).
 2879git_download_scheme(https).
 2880
 2881%!  github_release_url(+URL, -Pack, -Version:atom) is semidet.
 2882%
 2883%   True when URL is the URL of a GitHub release.  Such releases are
 2884%   accessible as
 2885%
 2886%       https:/github.com/<owner>/<pack>/archive/[vV]?<version>.zip'
 2887
 2888github_release_url(URL, Pack, Version) :-
 2889    uri_components(URL, Components),
 2890    uri_data(authority, Components, 'github.com'),
 2891    uri_data(scheme, Components, Scheme),
 2892    download_scheme(Scheme),
 2893    uri_data(path, Components, Path),
 2894    github_archive_path(Archive,Pack,File),
 2895    atomic_list_concat(Archive, /, Path),
 2896    file_name_extension(Tag, Ext, File),
 2897    github_archive_extension(Ext),
 2898    tag_version(Tag, Version),
 2899    !.
 2900
 2901github_archive_path(['',_User,Pack,archive,File],Pack,File).
 2902github_archive_path(['',_User,Pack,archive,refs,tags,File],Pack,File).
 2903
 2904github_archive_extension(tgz).
 2905github_archive_extension(zip).
 2906
 2907%!  tag_version(+GitTag, -Version) is semidet.
 2908%
 2909%   True when a GIT tag describes version Version.  GitTag must
 2910%   satisfy ``[vV]?int(\.int)*``.
 2911
 2912tag_version(Tag, Version) :-
 2913    version_tag_prefix(Prefix),
 2914    atom_concat(Prefix, Version, Tag),
 2915    is_version(Version).
 2916
 2917version_tag_prefix(v).
 2918version_tag_prefix('V').
 2919version_tag_prefix('').
 2920
 2921
 2922%!  git_archive_url(+URL, -Archive, +Options) is semidet.
 2923%
 2924%   If we do not have git installed, some git services offer downloading
 2925%   the code as  an  archive  using   HTTP.  This  predicate  makes this
 2926%   translation.
 2927
 2928git_archive_url(URL, Archive, Options) :-
 2929    uri_components(URL, Components),
 2930    uri_data(authority, Components, 'github.com'),
 2931    uri_data(path, Components, Path),
 2932    atomic_list_concat(['', User, RepoGit], /, Path),
 2933    $,
 2934    remove_git_ext(RepoGit, Repo),
 2935    git_archive_version(Version, Options),
 2936    atomic_list_concat(['', User, Repo, zip, Version], /, ArchivePath),
 2937    uri_edit([ path(ArchivePath),
 2938               host('codeload.github.com')
 2939             ],
 2940             URL, Archive).
 2941git_archive_url(URL, _, _) :-
 2942    print_message(error, pack(no_git(URL))),
 2943    fail.
 2944
 2945remove_git_ext(RepoGit, Repo) :-
 2946    file_name_extension(Repo, git, RepoGit),
 2947    !.
 2948remove_git_ext(Repo, Repo).
 2949
 2950git_archive_version(Version, Options) :-
 2951    option(commit(Version), Options),
 2952    !.
 2953git_archive_version(Version, Options) :-
 2954    option(branch(Version), Options),
 2955    !.
 2956git_archive_version(Version, Options) :-
 2957    option(version(Version), Options),
 2958    !.
 2959git_archive_version('HEAD', _).
 2960
 2961                 /*******************************
 2962                 *       QUERY CENTRAL DB       *
 2963                 *******************************/
 2964
 2965%!  publish_download(+Infos, +Options) is semidet.
 2966%!  register_downloads(+Infos, +Options) is det.
 2967%
 2968%   Register our downloads with the  pack server. The publish_download/2
 2969%   version is used to  register  a   specific  pack  after successfully
 2970%   installing the pack.  In this scenario, we
 2971%
 2972%     1. call register_downloads/2 with publish(Pack) that must be
 2973%        a no-op.
 2974%     2. build and test the pack
 2975%     3. call publish_download/2, which calls register_downloads/2
 2976%        after replacing publish(Pack) by do_publish(Pack).
 2977
 2978register_downloads(_, Options) :-
 2979    option(register(false), Options),
 2980    !.
 2981register_downloads(_, Options) :-
 2982    option(publish(_), Options),
 2983    !.
 2984register_downloads(Infos, Options) :-
 2985    convlist(download_data, Infos, Data),
 2986    (   Data == []
 2987    ->  true
 2988    ;   query_pack_server(downloaded(Data), Reply, Options),
 2989        (   option(do_publish(Pack), Options)
 2990        ->  (   member(Info, Infos),
 2991                Info.pack == Pack
 2992            ->  true
 2993            ),
 2994            (   Reply = true(Actions),
 2995                memberchk(Pack-Result, Actions)
 2996            ->  (   registered(Result)
 2997                ->  print_message(informational, pack(published(Info, Result)))
 2998                ;   print_message(error, pack(publish_failed(Info, Result))),
 2999                    fail
 3000                )
 3001            ;   print_message(error, pack(publish_failed(Info, false)))
 3002            )
 3003        ;   true
 3004        )
 3005    ).
 3006
 3007registered(git(_URL)).
 3008registered(file(_URL)).
 3009
 3010publish_download(Infos, Options) :-
 3011    select_option(publish(Pack), Options, Options1),
 3012    !,
 3013    register_downloads(Infos, [do_publish(Pack)|Options1]).
 3014publish_download(_Infos, _Options).
 3015
 3016%!  download_data(+Info, -Data) is semidet.
 3017%
 3018%   If we downloaded and installed Info, unify Data with the information
 3019%   that we share with the pack registry. That is a term
 3020%
 3021%       download(URL, Hash, Metadata).
 3022%
 3023%   Where URL is location of the GIT   repository or URL of the download
 3024%   archive. Hash is either the  GIT  commit   hash  or  the SHA1 of the
 3025%   archive file.
 3026
 3027download_data(Info, Data),
 3028    Info.get(git) == true =>                % Git clone
 3029    Data = download(URL, Hash, Metadata),
 3030    URL = Info.get(downloaded),
 3031    pack_git_info(Info.installed, Hash, Metadata).
 3032download_data(Info, Data),
 3033    _{git_url:URL,hash:Hash} :< Info, Hash \== (-) =>
 3034    Data = download(URL, Hash, Metadata),   % Git downloaded as zip
 3035    dir_metadata(Info.installed, Metadata).
 3036download_data(Info, Data) =>                % Archive download.
 3037    Data = download(URL, Hash, Metadata),
 3038    URL = Info.get(downloaded),
 3039    download_url(URL),
 3040    pack_status_dir(Info.installed, archive(Archive, URL)),
 3041    file_sha1(Archive, Hash),
 3042    pack_archive_info(Archive, _Pack, Metadata, _).
 3043
 3044%!  query_pack_server(+Query, -Result, +Options)
 3045%
 3046%   Send a Prolog query  to  the   package  server  and  process its
 3047%   results.
 3048
 3049query_pack_server(Query, Result, Options) :-
 3050    (   option(server(ServerOpt), Options)
 3051    ->  server_url(ServerOpt, ServerBase)
 3052    ;   setting(server, ServerBase),
 3053        ServerBase \== ''
 3054    ),
 3055    atom_concat(ServerBase, query, Server),
 3056    format(codes(Data), '~q.~n', Query),
 3057    info_level(Informational, Options),
 3058    print_message(Informational, pack(contacting_server(Server))),
 3059    setup_call_cleanup(
 3060        http_open(Server, In,
 3061                  [ post(codes(application/'x-prolog', Data)),
 3062                    header(content_type, ContentType)
 3063                  ]),
 3064        read_reply(ContentType, In, Result),
 3065        close(In)),
 3066    message_severity(Result, Level, Informational),
 3067    print_message(Level, pack(server_reply(Result))).
 3068
 3069server_url(URL0, URL) :-
 3070    uri_components(URL0, Components),
 3071    uri_data(scheme, Components, Scheme),
 3072    var(Scheme),
 3073    !,
 3074    atom_concat('https://', URL0, URL1),
 3075    server_url(URL1, URL).
 3076server_url(URL0, URL) :-
 3077    uri_components(URL0, Components),
 3078    uri_data(path, Components, ''),
 3079    !,
 3080    uri_edit([path('/pack/')], URL0, URL).
 3081server_url(URL, URL).
 3082
 3083read_reply(ContentType, In, Result) :-
 3084    sub_atom(ContentType, 0, _, _, 'application/x-prolog'),
 3085    !,
 3086    set_stream(In, encoding(utf8)),
 3087    read(In, Result).
 3088read_reply(ContentType, In, _Result) :-
 3089    read_string(In, 500, String),
 3090    print_message(error, pack(no_prolog_response(ContentType, String))),
 3091    fail.
 3092
 3093info_level(Level, Options) :-
 3094    option(silent(true), Options),
 3095    !,
 3096    Level = silent.
 3097info_level(informational, _).
 3098
 3099message_severity(true(_), Informational, Informational).
 3100message_severity(false, warning, _).
 3101message_severity(exception(_), error, _).
 3102
 3103
 3104                 /*******************************
 3105                 *        WILDCARD URIs         *
 3106                 *******************************/
 3107
 3108%!  available_download_versions(+URL, -Versions:list(atom), +Options) is det.
 3109%
 3110%   Deal with wildcard URLs, returning a  list of Version-URL pairs,
 3111%   sorted by version.
 3112%
 3113%   @tbd    Deal with protocols other than HTTP
 3114
 3115available_download_versions(URL, Versions, _Options) :-
 3116    wildcard_pattern(URL),
 3117    github_url(URL, User, Repo),            % demands https
 3118    !,
 3119    findall(Version-VersionURL,
 3120            github_version(User, Repo, Version, VersionURL),
 3121            Versions).
 3122available_download_versions(URL0, Versions, Options) :-
 3123    wildcard_pattern(URL0),
 3124    !,
 3125    hsts(URL0, URL, Options),
 3126    file_directory_name(URL, DirURL0),
 3127    ensure_slash(DirURL0, DirURL),
 3128    print_message(informational, pack(query_versions(DirURL))),
 3129    setup_call_cleanup(
 3130        http_open(DirURL, In, []),
 3131        load_html(stream(In), DOM,
 3132                  [ syntax_errors(quiet)
 3133                  ]),
 3134        close(In)),
 3135    findall(MatchingURL,
 3136            absolute_matching_href(DOM, URL, MatchingURL),
 3137            MatchingURLs),
 3138    (   MatchingURLs == []
 3139    ->  print_message(warning, pack(no_matching_urls(URL)))
 3140    ;   true
 3141    ),
 3142    versioned_urls(MatchingURLs, VersionedURLs),
 3143    sort_version_pairs(VersionedURLs, Versions),
 3144    print_message(informational, pack(found_versions(Versions))).
 3145available_download_versions(URL, [Version-URL], _Options) :-
 3146    (   pack_version_file(_Pack, Version0, URL)
 3147    ->  Version = Version0
 3148    ;   Version = '0.0.0'
 3149    ).
 3150
 3151%!  sort_version_pairs(+Pairs, -Sorted) is det.
 3152%
 3153%   Sort a list of Version-Data by decreasing version.
 3154
 3155sort_version_pairs(Pairs, Sorted) :-
 3156    map_list_to_pairs(version_pair_sort_key_, Pairs, Keyed),
 3157    sort(1, @>=, Keyed, SortedKeyed),
 3158    pairs_values(SortedKeyed, Sorted).
 3159
 3160version_pair_sort_key_(Version-_Data, Key) :-
 3161    version_sort_key(Version, Key).
 3162
 3163version_sort_key(Version, Key) :-
 3164    split_string(Version, ".", "", Parts),
 3165    maplist(number_string, Key, Parts),
 3166    !.
 3167version_sort_key(Version, _) :-
 3168    domain_error(version, Version).
 3169
 3170%!  github_url(+URL, -User, -Repo) is semidet.
 3171%
 3172%   True when URL refers to a github repository.
 3173
 3174github_url(URL, User, Repo) :-
 3175    uri_components(URL, uri_components(https,'github.com',Path,_,_)),
 3176    atomic_list_concat(['',User,Repo|_], /, Path).
 3177
 3178
 3179%!  github_version(+User, +Repo, -Version, -VersionURI) is nondet.
 3180%
 3181%   True when Version is a release version and VersionURI is the
 3182%   download location for the zip file.
 3183
 3184github_version(User, Repo, Version, VersionURI) :-
 3185    atomic_list_concat(['',repos,User,Repo,tags], /, Path1),
 3186    uri_components(ApiUri, uri_components(https,'api.github.com',Path1,_,_)),
 3187    setup_call_cleanup(
 3188      http_open(ApiUri, In,
 3189                [ request_header('Accept'='application/vnd.github.v3+json')
 3190                ]),
 3191      json_read_dict(In, Dicts),
 3192      close(In)),
 3193    member(Dict, Dicts),
 3194    atom_string(Tag, Dict.name),
 3195    tag_version(Tag, Version),
 3196    atom_string(VersionURI, Dict.zipball_url).
 3197
 3198wildcard_pattern(URL) :- sub_atom(URL, _, _, _, *).
 3199wildcard_pattern(URL) :- sub_atom(URL, _, _, _, ?).
 3200
 3201ensure_slash(Dir, DirS) :-
 3202    (   sub_atom(Dir, _, _, 0, /)
 3203    ->  DirS = Dir
 3204    ;   atom_concat(Dir, /, DirS)
 3205    ).
 3206
 3207remove_slash(Dir0, Dir) :-
 3208    Dir0 \== '/',
 3209    atom_concat(Dir1, /, Dir0),
 3210    !,
 3211    remove_slash(Dir1, Dir).
 3212remove_slash(Dir, Dir).
 3213
 3214absolute_matching_href(DOM, Pattern, Match) :-
 3215    xpath(DOM, //a(@href), HREF),
 3216    uri_normalized(HREF, Pattern, Match),
 3217    wildcard_match(Pattern, Match).
 3218
 3219versioned_urls([], []).
 3220versioned_urls([H|T0], List) :-
 3221    file_base_name(H, File),
 3222    (   pack_version_file(_Pack, Version, File)
 3223    ->  List = [Version-H|T]
 3224    ;   List = T
 3225    ),
 3226    versioned_urls(T0, T).
 3227
 3228
 3229                 /*******************************
 3230                 *          DEPENDENCIES        *
 3231                 *******************************/
 3232
 3233%!  pack_provides(?Pack, -Provides) is multi.
 3234%!  pack_requires(?Pack, -Requires) is nondet.
 3235%!  pack_conflicts(?Pack, -Conflicts) is nondet.
 3236%
 3237%   Provide logical access to pack dependency relations.
 3238
 3239pack_provides(Pack, Pack@Version) :-
 3240    current_pack(Pack),
 3241    once(pack_info(Pack, version, version(Version))).
 3242pack_provides(Pack, Provides) :-
 3243    findall(Prv, pack_info(Pack, dependency, provides(Prv)), PrvList),
 3244    member(Provides, PrvList).
 3245
 3246pack_requires(Pack, Requires) :-
 3247    current_pack(Pack),
 3248    findall(Req, pack_info(Pack, dependency, requires(Req)), ReqList),
 3249    member(Requires, ReqList).
 3250
 3251pack_conflicts(Pack, Conflicts) :-
 3252    current_pack(Pack),
 3253    findall(Cfl, pack_info(Pack, dependency, conflicts(Cfl)), CflList),
 3254    member(Conflicts, CflList).
 3255
 3256%!  pack_depends_on(?Pack, ?Dependency) is nondet.
 3257%
 3258%   True when Pack depends on pack   Dependency. This predicate does not
 3259%   deal with transitive dependency.
 3260
 3261pack_depends_on(Pack, Dependency) :-
 3262    ground(Pack),
 3263    !,
 3264    pack_requires(Pack, Requires),
 3265    \+ is_prolog_token(Requires),
 3266    pack_provides(Dependency, Provides),
 3267    satisfies_req(Provides, Requires).
 3268pack_depends_on(Pack, Dependency) :-
 3269    ground(Dependency),
 3270    !,
 3271    pack_provides(Dependency, Provides),
 3272    pack_requires(Pack, Requires),
 3273    satisfies_req(Provides, Requires).
 3274pack_depends_on(Pack, Dependency) :-
 3275    current_pack(Pack),
 3276    pack_depends_on(Pack, Dependency).
 3277
 3278%!  dependents(+Pack, -Dependents) is semidet.
 3279%
 3280%   True when Dependents is a list of  packs that (indirectly) depend on
 3281%   Pack.
 3282
 3283dependents(Pack, Deps) :-
 3284    setof(Dep, dependent(Pack, Dep, []), Deps).
 3285
 3286dependent(Pack, Dep, Seen) :-
 3287    pack_depends_on(Dep0, Pack),
 3288    \+ memberchk(Dep0, Seen),
 3289    (   Dep = Dep0
 3290    ;   dependent(Dep0, Dep, [Dep0|Seen])
 3291    ).
 3292
 3293%!  validate_dependencies is det.
 3294%
 3295%   Validate all dependencies, reporting on failures
 3296
 3297validate_dependencies :-
 3298    setof(Issue, pack_dependency_issue(_, Issue), Issues),
 3299    !,
 3300    print_message(warning, pack(dependency_issues(Issues))).
 3301validate_dependencies.
 3302
 3303%!  pack_dependency_issue(?Pack, -Issue) is nondet.
 3304%
 3305%   True when Issue is a dependency issue   regarding Pack. Issue is one
 3306%   of
 3307%
 3308%     - unsatisfied(Pack, Requires)
 3309%       The requirement Requires of Pack is not fulfilled.
 3310%     - conflicts(Pack, Conflict)
 3311%       Pack conflicts with Conflict.
 3312
 3313pack_dependency_issue(Pack, Issue) :-
 3314    current_pack(Pack),
 3315    pack_dependency_issue_(Pack, Issue).
 3316
 3317pack_dependency_issue_(Pack, unsatisfied(Pack, Requires)) :-
 3318    pack_requires(Pack, Requires),
 3319    (   is_prolog_token(Requires)
 3320    ->  \+ prolog_satisfies(Requires)
 3321    ;   \+ ( pack_provides(_, Provides),
 3322             satisfies_req(Provides, Requires) )
 3323    ).
 3324pack_dependency_issue_(Pack, conflicts(Pack, Conflicts)) :-
 3325    pack_conflicts(Pack, Conflicts),
 3326    (   is_prolog_token(Conflicts)
 3327    ->  prolog_satisfies(Conflicts)
 3328    ;   pack_provides(_, Provides),
 3329        satisfies_req(Provides, Conflicts)
 3330    ).
 3331
 3332
 3333		 /*******************************
 3334		 *      RECORD PACK FACTS	*
 3335		 *******************************/
 3336
 3337%!  pack_assert(+PackDir, ++Fact) is det.
 3338%
 3339%   Add/update  a  fact  about  packs.  These    facts   are  stored  in
 3340%   PackDir/status.db. Known facts are:
 3341%
 3342%     - built(Arch, Version, How)
 3343%       Pack has been built by SWI-Prolog Version for Arch.  How is one
 3344%       of `built` if we built it or `downloaded` if it was downloaded.
 3345%     - automatic(Boolean)
 3346%       If `true`, pack was installed as dependency.
 3347%     - archive(Archive, URL)
 3348%       Available when the pack was installed by unpacking Archive that
 3349%       was retrieved from URL.
 3350
 3351pack_assert(PackDir, Fact) :-
 3352    must_be(ground, Fact),
 3353    findall(Term, pack_status_dir(PackDir, Term), Facts0),
 3354    update_facts(Facts0, Fact, Facts),
 3355    OpenOptions = [encoding(utf8), lock(exclusive)],
 3356    status_file(PackDir, StatusFile),
 3357    (   Facts == Facts0
 3358    ->  true
 3359    ;   Facts0 \== [],
 3360        append(Facts0, New, Facts)
 3361    ->  setup_call_cleanup(
 3362            open(StatusFile, append, Out, OpenOptions),
 3363            maplist(write_fact(Out), New),
 3364            close(Out))
 3365    ;   setup_call_cleanup(
 3366            open(StatusFile, write, Out, OpenOptions),
 3367            ( write_facts_header(Out),
 3368              maplist(write_fact(Out), Facts)
 3369            ),
 3370            close(Out))
 3371    ).
 3372
 3373update_facts([], Fact, [Fact]) :-
 3374    !.
 3375update_facts([H|T], Fact, [Fact|T]) :-
 3376    general_pack_fact(Fact, GenFact),
 3377    general_pack_fact(H, GenTerm),
 3378    GenFact =@= GenTerm,
 3379    !.
 3380update_facts([H|T0], Fact, [H|T]) :-
 3381    update_facts(T0, Fact, T).
 3382
 3383general_pack_fact(built(Arch, _Version, _How), General) =>
 3384    General = built(Arch, _, _).
 3385general_pack_fact(Term, General), compound(Term) =>
 3386    compound_name_arity(Term, Name, Arity),
 3387    compound_name_arity(General, Name, Arity).
 3388general_pack_fact(Term, General) =>
 3389    General = Term.
 3390
 3391write_facts_header(Out) :-
 3392    format(Out, '% Fact status file.  Managed by package manager.~n', []).
 3393
 3394write_fact(Out, Term) :-
 3395    format(Out, '~q.~n', [Term]).
 3396
 3397%!  pack_status(?Pack, ?Fact).
 3398%!  pack_status_dir(+PackDir, ?Fact)
 3399%
 3400%   True when Fact is true about the package in PackDir.  Facts
 3401%   are asserted a file `status.db`.
 3402
 3403pack_status(Pack, Fact) :-
 3404    current_pack(Pack, PackDir),
 3405    pack_status_dir(PackDir, Fact).
 3406
 3407pack_status_dir(PackDir, Fact) :-
 3408    det_if(ground(Fact), pack_status_(PackDir, Fact)).
 3409
 3410pack_status_(PackDir, Fact) :-
 3411    status_file(PackDir, StatusFile),
 3412    catch(term_in_file(valid_term(pack_status_term), StatusFile, Fact),
 3413          error(existence_error(source_sink, StatusFile), _),
 3414          fail).
 3415
 3416pack_status_term(built(atom, version, oneof([built,downloaded]))).
 3417pack_status_term(automatic(boolean)).
 3418pack_status_term(archive(atom, atom)).
 3419
 3420
 3421%!  update_automatic(+Info) is det.
 3422%
 3423%   Update the _automatic_ status of a package.  If we install it has no
 3424%   automatic status and we install it  as   a  dependency we mark it as
 3425%   _automatic_. Else, we mark  it  as   non-automatic  as  it  has been
 3426%   installed explicitly.
 3427
 3428update_automatic(Info) :-
 3429    _ = Info.get(dependency_for),
 3430    \+ pack_status(Info.installed, automatic(_)),
 3431    !,
 3432    pack_assert(Info.installed, automatic(true)).
 3433update_automatic(Info) :-
 3434    pack_assert(Info.installed, automatic(false)).
 3435
 3436status_file(PackDir, StatusFile) :-
 3437    directory_file_path(PackDir, 'status.db', StatusFile).
 3438
 3439                 /*******************************
 3440                 *        USER INTERACTION      *
 3441                 *******************************/
 3442
 3443:- multifile prolog:message//1. 3444
 3445%!  menu(Question, +Alternatives, +Default, -Selection, +Options)
 3446
 3447menu(_Question, _Alternatives, Default, Selection, Options) :-
 3448    option(interactive(false), Options),
 3449    !,
 3450    Selection = Default.
 3451menu(Question, Alternatives, Default, Selection, _) :-
 3452    length(Alternatives, N),
 3453    between(1, 5, _),
 3454       print_message(query, Question),
 3455       print_menu(Alternatives, Default, 1),
 3456       print_message(query, pack(menu(select))),
 3457       read_selection(N, Choice),
 3458    !,
 3459    (   Choice == default
 3460    ->  Selection = Default
 3461    ;   nth1(Choice, Alternatives, Selection=_)
 3462    ->  true
 3463    ).
 3464
 3465print_menu([], _, _).
 3466print_menu([Value=Label|T], Default, I) :-
 3467    (   Value == Default
 3468    ->  print_message(query, pack(menu(default_item(I, Label))))
 3469    ;   print_message(query, pack(menu(item(I, Label))))
 3470    ),
 3471    I2 is I + 1,
 3472    print_menu(T, Default, I2).
 3473
 3474read_selection(Max, Choice) :-
 3475    get_single_char(Code),
 3476    (   answered_default(Code)
 3477    ->  Choice = default
 3478    ;   code_type(Code, digit(Choice)),
 3479        between(1, Max, Choice)
 3480    ->  true
 3481    ;   print_message(warning, pack(menu(reply(1,Max)))),
 3482        fail
 3483    ).
 3484
 3485%!  confirm(+Question, +Default, +Options) is semidet.
 3486%
 3487%   Ask for confirmation.
 3488%
 3489%   @arg Default is one of `yes`, `no` or `none`.
 3490
 3491confirm(_Question, Default, Options) :-
 3492    Default \== none,
 3493    option(interactive(false), Options, true),
 3494    !,
 3495    Default == yes.
 3496confirm(Question, Default, _) :-
 3497    between(1, 5, _),
 3498       print_message(query, pack(confirm(Question, Default))),
 3499       read_yes_no(YesNo, Default),
 3500    !,
 3501    format(user_error, '~N', []),
 3502    YesNo == yes.
 3503
 3504read_yes_no(YesNo, Default) :-
 3505    get_single_char(Code),
 3506    code_yes_no(Code, Default, YesNo),
 3507    !.
 3508
 3509code_yes_no(0'y, _, yes).
 3510code_yes_no(0'Y, _, yes).
 3511code_yes_no(0'n, _, no).
 3512code_yes_no(0'N, _, no).
 3513code_yes_no(_, none, _) :- !, fail.
 3514code_yes_no(C, Default, Default) :-
 3515    answered_default(C).
 3516
 3517answered_default(0'\r).
 3518answered_default(0'\n).
 3519answered_default(0'\s).
 3520
 3521
 3522                 /*******************************
 3523                 *            MESSAGES          *
 3524                 *******************************/
 3525
 3526:- multifile prolog:message//1. 3527
 3528prolog:message(pack(Message)) -->
 3529    message(Message).
 3530
 3531:- discontiguous
 3532    message//1,
 3533    label//1. 3534
 3535message(invalid_term(pack_info_term, Term)) -->
 3536    [ 'Invalid package meta data: ~q'-[Term] ].
 3537message(invalid_term(pack_status_term, Term)) -->
 3538    [ 'Invalid package status data: ~q'-[Term] ].
 3539message(directory_exists(Dir)) -->
 3540    [ 'Package target directory exists and is not empty:', nl,
 3541      '\t~q'-[Dir]
 3542    ].
 3543message(already_installed(pack(Pack, Version))) -->
 3544    [ 'Pack `~w'' is already installed @~w'-[Pack, Version] ].
 3545message(already_installed(Pack)) -->
 3546    [ 'Pack `~w'' is already installed. Package info:'-[Pack] ].
 3547message(kept_foreign(Pack, Arch)) -->
 3548    [ 'Found foreign libraries for architecture '-[],
 3549      ansi(code, '~q', [Arch]), nl,
 3550      'Use ', ansi(code, '?- pack_rebuild(~q).', [Pack]),
 3551      ' to rebuild from sources'-[]
 3552    ].
 3553message(no_pack_installed(Pack)) -->
 3554    [ 'No pack ~q installed.  Use ?- pack_list(Pattern) to search'-[Pack] ].
 3555message(dependency_issues(Issues)) -->
 3556    [ 'The current set of packs has dependency issues:', nl ],
 3557    dep_issues(Issues).
 3558message(depends(Pack, Deps)) -->
 3559    [ 'The following packs depend on `~w\':'-[Pack], nl ],
 3560    pack_list(Deps).
 3561message(remove(link(To), PackDir)) -->
 3562    [ 'Removing ', url(PackDir), nl, '    as link to ', url(To) ].
 3563message(remove(directory, PackDir)) -->
 3564    [ 'Removing ~q and contents'-[PackDir] ].
 3565message(remove_existing_pack(PackDir)) -->
 3566    [ 'Remove old installation in ~q'-[PackDir] ].
 3567message(delete_autoload_index(Pack, Index)) -->
 3568    [ 'Pack ' ], msg_pack(Pack), [ ': deleting autoload index ', url(Index) ].
 3569message(download_plan(Plan)) -->
 3570    [ ansi(bold, 'Installation plan:', []), nl ],
 3571    install_plan(Plan, Actions),
 3572    install_label(Actions).
 3573message(build_plan(Plan)) -->
 3574    [ ansi(bold, 'The following packs have post install scripts:', []), nl ],
 3575    msg_build_plan(Plan),
 3576    [ nl, ansi(bold, 'Run scripts?', []) ].
 3577message(autoload(Pack)) -->
 3578    [ 'Pack ' ], msg_pack(Pack),
 3579    [ ' prefers to be added as autoload library',
 3580      nl, ansi(bold, 'Allow?', [])
 3581    ].
 3582message(no_meta_data(BaseDir)) -->
 3583    [ 'Cannot find pack.pl inside directory ~q.  Not a package?'-[BaseDir] ].
 3584message(search_no_matches(Name)) -->
 3585    [ 'Search for "~w", returned no matching packages'-[Name] ].
 3586message(rebuild(Pack)) -->
 3587    [ 'Checking pack "~w" for rebuild ...'-[Pack] ].
 3588message(up_to_date([Pack])) -->
 3589    !,
 3590    [ 'Pack ' ], msg_pack(Pack), [' is up-to-date' ].
 3591message(up_to_date(Packs)) -->
 3592    [ 'Packs ' ], sequence(msg_pack, [', '], Packs), [' are up-to-date' ].
 3593message(installed_can_upgrade(List)) -->
 3594    sequence(msg_can_upgrade_target, [nl], List).
 3595message(new_dependencies(Deps)) -->
 3596    [ 'Found new dependencies after downloading (~p).'-[Deps], nl ].
 3597message(query_versions(URL)) -->
 3598    [ 'Querying "~w" to find new versions ...'-[URL] ].
 3599message(no_matching_urls(URL)) -->
 3600    [ 'Could not find any matching URL: ~q'-[URL] ].
 3601message(found_versions([Latest-_URL|More])) -->
 3602    { length(More, Len) },
 3603    [ '    Latest version: ~w (~D older)'-[Latest, Len] ].
 3604message(build(Pack, PackDir)) -->
 3605    [ ansi(bold, 'Building pack ~w in directory ~w', [Pack, PackDir]) ].
 3606message(contacting_server(Server)) -->
 3607    [ 'Contacting server at ~w ...'-[Server], flush ].
 3608message(server_reply(true(_))) -->
 3609    [ at_same_line, ' ok'-[] ].
 3610message(server_reply(false)) -->
 3611    [ at_same_line, ' done'-[] ].
 3612message(server_reply(exception(E))) -->
 3613    [ 'Server reported the following error:'-[], nl ],
 3614    '$messages':translate_message(E).
 3615message(cannot_create_dir(Alias)) -->
 3616    { findall(PackDir,
 3617              absolute_file_name(Alias, PackDir, [solutions(all)]),
 3618              PackDirs0),
 3619      sort(PackDirs0, PackDirs)
 3620    },
 3621    [ 'Cannot find a place to create a package directory.'-[],
 3622      'Considered:'-[]
 3623    ],
 3624    candidate_dirs(PackDirs).
 3625message(conflict(version, [PackV, FileV])) -->
 3626    ['Version mismatch: pack.pl: '-[]], msg_version(PackV),
 3627    [', file claims version '-[]], msg_version(FileV).
 3628message(conflict(name, [PackInfo, FileInfo])) -->
 3629    ['Pack ~w mismatch: pack.pl: ~p'-[PackInfo]],
 3630    [', file claims ~w: ~p'-[FileInfo]].
 3631message(no_prolog_response(ContentType, String)) -->
 3632    [ 'Expected Prolog response.  Got content of type ~p'-[ContentType], nl,
 3633      '~s'-[String]
 3634    ].
 3635message(download(begin, Pack, _URL, _DownloadFile)) -->
 3636    [ 'Downloading ' ], msg_pack(Pack), [ ' ... ', flush ].
 3637message(download(end, _, _, File)) -->
 3638    { size_file(File, Bytes) },
 3639    [ at_same_line, '~D bytes'-[Bytes] ].
 3640message(no_git(URL)) -->
 3641    [ 'Cannot install from git repository ', url(URL), '.', nl,
 3642      'Cannot find git program and do not know how to download the code', nl,
 3643      'from this git service.  Please install git and retry.'
 3644    ].
 3645message(git_no_https(GitURL)) -->
 3646    [ 'Do not know how to get an HTTP(s) URL for ', url(GitURL) ].
 3647message(git_branch_not_default(Dir, Default, Current)) -->
 3648    [ 'GIT current branch on ', url(Dir), ' is not default.', nl,
 3649      '  Current branch: ', ansi(code, '~w', [Current]),
 3650      ' default: ', ansi(code, '~w', [Default])
 3651    ].
 3652message(git_not_clean(Dir)) -->
 3653    [ 'GIT working directory is dirty: ', url(Dir), nl,
 3654      'Your repository must be clean before publishing.'
 3655    ].
 3656message(git_push) -->
 3657    [ 'Push release to GIT origin?' ].
 3658message(git_tag(Tag)) -->
 3659    [ 'Tag repository with release tag ', ansi(code, '~w', [Tag]) ].
 3660message(git_release_tag_not_at_head(Tag)) -->
 3661    [ 'Release tag ', ansi(code, '~w', [Tag]), ' is not at HEAD.', nl,
 3662      'If you want to update the tag, please run ',
 3663      ansi(code, 'git tag -d ~w', [Tag])
 3664    ].
 3665message(git_tag_out_of_sync(Tag)) -->
 3666    [ 'Release tag ', ansi(code, '~w', [Tag]),
 3667      ' differs from this tag at the origin'
 3668    ].
 3669
 3670message(published(Info, At)) -->
 3671    [ 'Published pack ' ], msg_pack(Info), msg_info_version(Info),
 3672    [' to be installed from '],
 3673    msg_published_address(At).
 3674message(publish_failed(Info, Reason)) -->
 3675    [ 'Pack ' ], msg_pack(Info), [ ' at version ~w'-[Info.version] ],
 3676    msg_publish_failed(Reason).
 3677
 3678msg_publish_failed(throw(error(permission_error(register,
 3679                                                pack(_),_URL),_))) -->
 3680    [ ' is already registered with a different URL'].
 3681msg_publish_failed(download) -->
 3682    [' was already published?'].
 3683msg_publish_failed(Status) -->
 3684    [ ' failed for unknown reason (~p)'-[Status] ].
 3685
 3686msg_published_address(git(URL)) -->
 3687    msg_url(URL, _).
 3688msg_published_address(file(URL)) -->
 3689    msg_url(URL, _).
 3690
 3691candidate_dirs([]) --> [].
 3692candidate_dirs([H|T]) --> [ nl, '    ~w'-[H] ], candidate_dirs(T).
 3693                                                % Questions
 3694message(resolve_remove) -->
 3695    [ nl, 'Please select an action:', nl, nl ].
 3696message(create_pack_dir) -->
 3697    [ nl, 'Create directory for packages', nl ].
 3698message(menu(item(I, Label))) -->
 3699    [ '~t(~d)~6|   '-[I] ],
 3700    label(Label).
 3701message(menu(default_item(I, Label))) -->
 3702    [ '~t(~d)~6| * '-[I] ],
 3703    label(Label).
 3704message(menu(select)) -->
 3705    [ nl, 'Your choice? ', flush ].
 3706message(confirm(Question, Default)) -->
 3707    message(Question),
 3708    confirm_default(Default),
 3709    [ flush ].
 3710message(menu(reply(Min,Max))) -->
 3711    (  { Max =:= Min+1 }
 3712    -> [ 'Please enter ~w or ~w'-[Min,Max] ]
 3713    ;  [ 'Please enter a number between ~w and ~w'-[Min,Max] ]
 3714    ).
 3715
 3716                                                % support predicates
 3717dep_issues(Issues) -->
 3718    sequence(dep_issue, [nl], Issues).
 3719
 3720dep_issue(unsatisfied(Pack, Requires)) -->
 3721    [ ' - Pack ' ], msg_pack(Pack), [' requires ~p'-[Requires]].
 3722dep_issue(conflicts(Pack, Conflict)) -->
 3723    [ ' - Pack ' ], msg_pack(Pack), [' conflicts with ~p'-[Conflict]].
 3724
 3725%!  install_plan(+Plan, -Actions)// is det.
 3726%!  install_label(+Actions)// is det.
 3727%
 3728%   Describe the overall installation plan before downloading.
 3729
 3730install_label([link]) -->
 3731    !,
 3732    [ ansi(bold, 'Activate pack?', []) ].
 3733install_label([unpack]) -->
 3734    !,
 3735    [ ansi(bold, 'Unpack archive?', []) ].
 3736install_label(_) -->
 3737    [ ansi(bold, 'Download packs?', []) ].
 3738
 3739
 3740install_plan(Plan, Actions) -->
 3741    install_plan(Plan, Actions, Sec),
 3742    sec_warning(Sec).
 3743
 3744install_plan([], [], _) -->
 3745    [].
 3746install_plan([H|T], [AH|AT], Sec) -->
 3747    install_step(H, AH, Sec), [nl],
 3748    install_plan(T, AT, Sec).
 3749
 3750install_step(Info, keep, _Sec) -->
 3751    { Info.get(keep) == true },
 3752    !,
 3753    [ '  Keep ' ], msg_pack(Info), [ ' at version ~w'-[Info.version] ],
 3754    msg_can_upgrade(Info).
 3755install_step(Info, Action, Sec) -->
 3756    { From = Info.get(upgrade),
 3757      VFrom = From.version,
 3758      VTo = Info.get(version),
 3759      (   cmp_versions(>=, VTo, VFrom)
 3760      ->  Label = ansi(bold,    '  Upgrade ',   [])
 3761      ;   Label = ansi(warning, '  Downgrade ', [])
 3762      )
 3763    },
 3764    [ Label ], msg_pack(Info),
 3765    [ ' from version ~w to ~w'- [From.version, Info.get(version)] ],
 3766    install_from(Info, Action, Sec).
 3767install_step(Info, Action, Sec) -->
 3768    { _From = Info.get(upgrade) },
 3769    [ '  Upgrade '  ], msg_pack(Info),
 3770    install_from(Info, Action, Sec).
 3771install_step(Info, Action, Sec) -->
 3772    { Dep = Info.get(dependency_for) },
 3773    [ '  Install ' ], msg_pack(Info),
 3774    [ ' at version ~w as dependency for '-[Info.version],
 3775      ansi(code, '~w', [Dep])
 3776    ],
 3777    install_from(Info, Action, Sec),
 3778    msg_downloads(Info).
 3779install_step(Info, Action, Sec) -->
 3780    { Info.get(commit) == 'HEAD' },
 3781    !,
 3782    [ '  Install ' ], msg_pack(Info), [ ' at current GIT HEAD'-[] ],
 3783    install_from(Info, Action, Sec),
 3784    msg_downloads(Info).
 3785install_step(Info, link, _Sec) -->
 3786    { Info.get(link) == true,
 3787      uri_file_name(Info.get(url), Dir)
 3788    },
 3789    !,
 3790    [ '  Install ' ], msg_pack(Info), [ ' as symlink to ', url(Dir) ].
 3791install_step(Info, Action, Sec) -->
 3792    [ '  Install ' ], msg_pack(Info), [ ' at version ~w'-[Info.get(version)] ],
 3793    install_from(Info, Action, Sec),
 3794    msg_downloads(Info).
 3795install_step(Info, Action, Sec) -->
 3796    [ '  Install ' ], msg_pack(Info),
 3797    install_from(Info, Action, Sec),
 3798    msg_downloads(Info).
 3799
 3800install_from(Info, download, Sec) -->
 3801    { download_url(Info.url) },
 3802    !,
 3803    [ ' from '  ], msg_url(Info.url, Sec).
 3804install_from(Info, unpack, Sec) -->
 3805    [ ' from '  ], msg_url(Info.url, Sec).
 3806
 3807msg_url(URL, unsafe) -->
 3808    { atomic(URL),
 3809      atom_concat('http://', Rest, URL)
 3810    },
 3811    [ ansi(error, '~w', ['http://']), '~w'-[Rest] ].
 3812msg_url(URL, _) -->
 3813    [ url(URL) ].
 3814
 3815sec_warning(Sec) -->
 3816    { var(Sec) },
 3817    !.
 3818sec_warning(unsafe) -->
 3819    [ ansi(warning, '  WARNING: The installation plan includes downloads \c
 3820                                from insecure HTTP servers.', []), nl
 3821    ].
 3822
 3823msg_downloads(Info) -->
 3824    { Downloads = Info.get(all_downloads),
 3825      Downloads > 0
 3826    },
 3827    [ ansi(comment, ' (downloaded ~D times)', [Downloads]) ],
 3828    !.
 3829msg_downloads(_) -->
 3830    [].
 3831
 3832msg_pack(Pack) -->
 3833    { atom(Pack) },
 3834    !,
 3835    [ ansi(code, '~w', [Pack]) ].
 3836msg_pack(Info) -->
 3837    msg_pack(Info.pack).
 3838
 3839msg_info_version(Info) -->
 3840    [ ansi(code, '@~w', [Info.get(version)]) ],
 3841    !.
 3842msg_info_version(_Info) -->
 3843    [].
 3844
 3845%!  msg_build_plan(+Plan)//
 3846%
 3847%   Describe the build plan before running the build steps.
 3848
 3849msg_build_plan(Plan) -->
 3850    sequence(build_step, [nl], Plan).
 3851
 3852build_step(Info) -->
 3853    [ '  Build ' ], msg_pack(Info), [' in directory ', url(Info.installed) ].
 3854
 3855msg_can_upgrade_target(Info) -->
 3856    [ '  Pack ' ], msg_pack(Info),
 3857    [ ' is installed at version ~w'-[Info.version] ],
 3858    msg_can_upgrade(Info).
 3859
 3860pack_list([]) --> [].
 3861pack_list([H|T]) -->
 3862    [ '    - Pack ' ],  msg_pack(H), [nl],
 3863    pack_list(T).
 3864
 3865label(remove_only(Pack)) -->
 3866    [ 'Only remove package ~w (break dependencies)'-[Pack] ].
 3867label(remove_deps(Pack, Deps)) -->
 3868    { length(Deps, Count) },
 3869    [ 'Remove package ~w and ~D dependencies'-[Pack, Count] ].
 3870label(create_dir(Dir)) -->
 3871    [ '~w'-[Dir] ].
 3872label(install_from(git(URL))) -->
 3873    !,
 3874    [ 'GIT repository at ~w'-[URL] ].
 3875label(install_from(URL)) -->
 3876    [ '~w'-[URL] ].
 3877label(cancel) -->
 3878    [ 'Cancel' ].
 3879
 3880confirm_default(yes) -->
 3881    [ ' Y/n? ' ].
 3882confirm_default(no) -->
 3883    [ ' y/N? ' ].
 3884confirm_default(none) -->
 3885    [ ' y/n? ' ].
 3886
 3887msg_version(Version) -->
 3888    [ '~w'-[Version] ].
 3889
 3890msg_can_upgrade(Info) -->
 3891    { Latest = Info.get(latest_version) },
 3892    [ ansi(warning, ' (can be upgraded to ~w)', [Latest]) ].
 3893msg_can_upgrade(_) -->
 3894    [].
 3895
 3896
 3897		 /*******************************
 3898		 *              MISC		*
 3899		 *******************************/
 3900
 3901local_uri_file_name(URL, FileName) :-
 3902    uri_file_name(URL, FileName),
 3903    !.
 3904local_uri_file_name(URL, FileName) :-
 3905    uri_components(URL, Components),
 3906    uri_data(scheme, Components, File), File == file,
 3907    uri_data(authority, Components, FileNameEnc),
 3908    uri_data(path, Components, ''),
 3909    uri_encoded(path, FileName, FileNameEnc).
 3910
 3911det_if(Cond, Goal) :-
 3912    (   Cond
 3913    ->  Goal,
 3914        !
 3915    ;   Goal
 3916    ).
 3917
 3918member_nonvar(_, Var) :-
 3919    var(Var),
 3920    !,
 3921    fail.
 3922member_nonvar(E, [E|_]).
 3923member_nonvar(E, [_|T]) :-
 3924    member_nonvar(E, T)