aboutsummaryrefslogtreecommitdiff
path: root/mod/mod-package-version-details.cxx
diff options
context:
space:
mode:
Diffstat (limited to 'mod/mod-package-version-details.cxx')
-rw-r--r--mod/mod-package-version-details.cxx570
1 files changed, 443 insertions, 127 deletions
diff --git a/mod/mod-package-version-details.cxx b/mod/mod-package-version-details.cxx
index 8787860..91923e5 100644
--- a/mod/mod-package-version-details.cxx
+++ b/mod/mod-package-version-details.cxx
@@ -1,5 +1,4 @@
// file : mod/mod-package-version-details.cxx -*- C++ -*-
-// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd
// license : MIT; see accompanying LICENSE file
#include <mod/mod-package-version-details.hxx>
@@ -10,9 +9,12 @@
#include <odb/database.hxx>
#include <odb/transaction.hxx>
-#include <web/xhtml.hxx>
-#include <web/module.hxx>
-#include <web/mime-url-encoding.hxx>
+#include <libbutl/filesystem.hxx> // dir_iterator, dir_entry
+
+#include <web/server/module.hxx>
+#include <web/server/mime-url-encoding.hxx>
+
+#include <web/xhtml/serialization.hxx>
#include <libbrep/build.hxx>
#include <libbrep/build-odb.hxx>
@@ -20,7 +22,7 @@
#include <libbrep/package-odb.hxx>
#include <mod/page.hxx>
-#include <mod/options.hxx>
+#include <mod/module-options.hxx>
using namespace std;
using namespace butl;
@@ -47,6 +49,12 @@ init (scanner& s)
options_ = make_shared<options::package_version_details> (
s, unknown_mode::fail, unknown_mode::fail);
+ // Verify that the bindist-url option is specified when necessary.
+ //
+ if (options_->bindist_root_specified () &&
+ !options_->bindist_url_specified ())
+ fail << "bindist-url must be specified if bindist-root is specified";
+
database_module::init (static_cast<const options::package_db&> (*options_),
options_->package_db_retry ());
@@ -152,7 +160,7 @@ handle (request& rq, response& rs)
const string& name (pkg->name.string ());
- const string title (name + " " + sver);
+ const string title (name + ' ' + sver);
xml::serializer s (rs.content (), title);
s << HTML
@@ -181,20 +189,20 @@ handle (request& rq, response& rs)
s << H2 << pkg->summary << ~H2;
- if (const optional<string>& d = pkg->description)
+ if (const optional<typed_text>& d = pkg->package_description
+ ? pkg->package_description
+ : pkg->description)
{
const string id ("description");
const string what (title + " description");
s << (full
- ? DIV_TEXT (*d, *
- pkg->description_type,
+ ? DIV_TEXT (*d,
true /* strip_title */,
id,
what,
error)
: DIV_TEXT (*d,
- *pkg->description_type,
true /* strip_title */,
options_->package_description (),
url (!full, id),
@@ -214,14 +222,13 @@ handle (request& rq, response& rs)
<< TR_PRIORITY (pkg->priority)
<< TR_LICENSES (pkg->license_alternatives)
- << TR_REPOSITORY (rl.canonical_name (), root, tenant)
- << TR_LOCATION (rl);
+ << TR_REPOSITORY (rl, root, tenant);
if (rl.type () == repository_type::pkg)
{
assert (pkg->location);
- s << TR_LINK (rl.url ().string () + "/" + pkg->location->string (),
+ s << TR_LINK (rl.url ().string () + '/' + pkg->location->string (),
pkg->location->leaf ().string (),
"download");
}
@@ -293,7 +300,7 @@ handle (request& rq, response& rs)
if (dcon)
s << ' '
- << A(HREF=u + "/" + p->version.string ()) << *dcon << ~A;
+ << A(HREF=u + '/' + p->version.string ()) << *dcon << ~A;
}
else if (p->internal ())
{
@@ -321,31 +328,51 @@ handle (request& rq, response& rs)
<< TABLE(CLASS="proplist", ID="depends")
<< TBODY;
- for (const auto& da: ds)
+ for (const auto& das: ds)
{
s << TR(CLASS="depends")
<< TH;
- if (da.conditional)
- s << "?";
-
- if (da.buildtime)
- s << "*";
+ if (das.buildtime)
+ s << '*';
s << ~TH
<< TD
<< SPAN(CLASS="value");
- for (const auto& d: da)
+ for (const auto& da: das)
{
- if (&d != &da[0])
+ if (&da != &das[0])
s << " | ";
- print_dependency (d);
+ // Should we enclose multiple dependencies into curly braces as in the
+ // manifest? Somehow feels redundant here, since there can't be any
+ // ambiguity (dependency group version constraint is already punched
+ // into the specific dependencies without constraints).
+ //
+ for (const dependency& d: da)
+ {
+ if (&d != &da[0])
+ s << ' ';
+
+ print_dependency (d);
+ }
+
+ if (da.enable)
+ {
+ s << " ? (";
+
+ if (full)
+ s << *da.enable;
+ else
+ s << "...";
+
+ s << ')';
+ }
}
s << ~SPAN
- << SPAN_COMMENT (da.comment)
+ << SPAN_COMMENT (das.comment)
<< ~TD
<< ~TR;
}
@@ -361,34 +388,59 @@ handle (request& rq, response& rs)
<< TABLE(CLASS="proplist", ID="requires")
<< TBODY;
- for (const auto& ra: rm)
+ for (const requirement_alternatives& ras: rm)
{
s << TR(CLASS="requires")
<< TH;
- if (ra.conditional)
- s << "?";
-
- if (ra.buildtime)
- s << "*";
-
- if (ra.conditional || ra.buildtime)
- s << " ";
+ if (ras.buildtime)
+ s << '*';
s << ~TH
<< TD
<< SPAN(CLASS="value");
- for (const auto& r: ra)
+ for (const requirement_alternative& ra: ras)
{
- if (&r != &ra[0])
+ if (&ra != &ras[0])
s << " | ";
- s << r;
+ // Should we enclose multiple requirement ids into curly braces as in
+ // the manifest? Somehow feels redundant here, since there can't be
+ // any ambiguity (requirement group version constraint is already
+ // punched into the specific requirements without constraints).
+ //
+ for (const string& r: ra)
+ {
+ if (&r != &ra[0])
+ s << ' ';
+
+ s << r;
+ }
+
+ if (ra.enable)
+ {
+ if (!ra.simple () || !ra[0].empty ())
+ s << ' ';
+
+ s << '?';
+
+ if (!ra.enable->empty ())
+ {
+ s << " (";
+
+ if (full)
+ s << *ra.enable;
+ else
+ s << "...";
+
+ s << ')';
+ }
+ }
}
s << ~SPAN
- << SPAN_COMMENT (ra.comment)
+ << SPAN_COMMENT (ras.comment)
<< ~TD
<< ~TR;
}
@@ -397,38 +449,84 @@ handle (request& rq, response& rs)
<< ~TABLE;
}
- auto print_dependencies = [&s, &print_dependency]
- (const small_vector<dependency, 1>& deps,
- const char* heading,
- const char* id)
+ // Print the test dependencies grouped by types as the separate blocks.
+ //
+ // Print test dependencies of the specific type.
+ //
+ auto print_tests = [&pkg,
+ &s,
+ &print_dependency,
+ full] (test_dependency_type dt)
{
- if (!deps.empty ())
- {
- s << H3 << heading << ~H3
- << TABLE(CLASS="proplist", ID=id)
- << TBODY;
+ string id;
- for (const dependency& d: deps)
+ bool first (true);
+ for (const test_dependency& td: pkg->tests)
+ {
+ if (td.type == dt)
{
+ // Print the table header if this is a first test dependency.
+ //
+ if (first)
+ {
+ id = to_string (dt);
+
+ // Capitalize the heading.
+ //
+ string heading (id);
+ heading[0] = ucase (id[0]);
+
+ s << H3 << heading << ~H3
+ << TABLE(CLASS="proplist", ID=id)
+ << TBODY;
+
+ first = false;
+ }
+
s << TR(CLASS=id)
+ << TH;
+
+ if (td.buildtime)
+ s << '*';
+
+ s << ~TH
<< TD
<< SPAN(CLASS="value");
- print_dependency (d);
+ print_dependency (td);
+
+ if (td.enable || td.reflect)
+ {
+ if (full)
+ {
+ if (td.enable)
+ s << " ? (" << *td.enable << ')';
+
+ if (td.reflect)
+ s << ' ' << *td.reflect;
+ }
+ else
+ s << " ...";
+ }
s << ~SPAN
<< ~TD
<< ~TR;
}
+ }
+ // Print the table closing tags if it was printed.
+ //
+ if (!first)
+ {
s << ~TBODY
<< ~TABLE;
}
};
- print_dependencies (pkg->tests, "Tests", "tests");
- print_dependencies (pkg->examples, "Examples", "examples");
- print_dependencies (pkg->benchmarks, "Benchmarks", "benchmarks");
+ print_tests (test_dependency_type::tests);
+ print_tests (test_dependency_type::examples);
+ print_tests (test_dependency_type::benchmarks);
bool builds (build_db_ != nullptr && pkg->buildable);
@@ -436,34 +534,203 @@ handle (request& rq, response& rs)
{
package_db_->load (*pkg, pkg->build_section);
- // If the package has a singe build configuration class expression with
- // exactly one underlying class and the class is none, then we just drop
- // the page builds section altogether.
+ // If all package build configurations has a singe effective build
+ // configuration class expression with exactly one underlying class and
+ // the class is none, then we just drop the page builds section
+ // altogether.
//
- if (pkg->builds.size () == 1)
+ builds = false;
+
+ for (const package_build_config& pc: pkg->build_configs)
{
- const build_class_expr& be (pkg->builds[0]);
+ const build_class_exprs& exprs (pc.effective_builds (pkg->builds));
- builds = be.underlying_classes.size () != 1 ||
- be.underlying_classes[0] != "none";
+ if (exprs.size () != 1 ||
+ exprs[0].underlying_classes.size () != 1 ||
+ exprs[0].underlying_classes[0] != "none")
+ {
+ builds = true;
+ break;
+ }
}
}
- bool archived (package_db_->load<brep::tenant> (tenant)->archived);
+ shared_ptr<brep::tenant> tn (package_db_->load<brep::tenant> (tenant));
t.commit ();
- if (builds)
+ // Display the binary distribution packages for this tenant, package, and
+ // version, if present. Print the archive distributions last.
+ //
+ if (options_->bindist_root_specified ())
{
- using bbot::build_config;
+ // Collect all the available package configurations by iterating over the
+ // <distribution> and <os-release> subdirectories and the <package-config>
+ // symlinks in the following filesystem hierarchy:
+ //
+ // [<tenant>/]<distribution>/<os-release>/<project>/<package>/<version>/<package-config>
+ //
+ // Note that it is possible that new directories and symlinks are created
+ // and/or removed while we iterate over the filesystem entries in the
+ // above hierarchy, which may result with system_error exceptions. If that
+ // happens, we just ignore such exceptions, trying to collect what we can.
+ //
+ const dir_path& br (options_->bindist_root ());
+
+ dir_path d (br);
+
+ if (!tenant.empty ())
+ d /= tenant;
+
+ // Note that distribution and os_release are simple paths and the
+ // config_symlink and config_dir are relative to the bindist root
+ // directory.
+ //
+ struct bindist_config
+ {
+ dir_path distribution; // debian, fedora, archive
+ dir_path os_release; // fedora37, windows10
+ path symlink; // .../x86_64, .../x86_64-release
+ dir_path directory; // .../x86_64-2023-05-11T10:13:43Z
+
+ bool
+ operator< (const bindist_config& v)
+ {
+ if (int r = distribution.compare (v.distribution))
+ return distribution.string () == "archive" ? false :
+ v.distribution.string () == "archive" ? true :
+ r < 0;
+
+ if (int r = os_release.compare (v.os_release))
+ return r < 0;
+
+ return symlink < v.symlink;
+ }
+ };
+
+ vector<bindist_config> configs;
+
+ if (dir_exists (d))
+ try
+ {
+ for (const dir_entry& de: dir_iterator (d, dir_iterator::ignore_dangling))
+ {
+ if (de.type () != entry_type::directory)
+ continue;
+ // Distribution directory.
+ //
+ dir_path dd (path_cast<dir_path> (de.path ()));
+
+ try
+ {
+ dir_path fdd (d / dd);
+
+ for (const dir_entry& re:
+ dir_iterator (fdd, dir_iterator::ignore_dangling))
+ {
+ if (re.type () != entry_type::directory)
+ continue;
+
+ // OS release directory.
+ //
+ dir_path rd (path_cast<dir_path> (re.path ()));
+
+ // Package version directory.
+ //
+ dir_path vd (fdd /
+ rd /
+ dir_path (pkg->project.string ()) /
+ dir_path (pn.string ()) /
+ dir_path (sver));
+
+ try
+ {
+ for (const dir_entry& ce:
+ dir_iterator (vd, dir_iterator::ignore_dangling))
+ {
+ if (ce.ltype () != entry_type::symlink)
+ continue;
+
+ // Skip the "hidden" symlinks which may potentially be used by
+ // the upload handlers until they expose the finalized upload
+ // directory.
+ //
+ const path& cl (ce.path ());
+ if (cl.string () [0] == '.')
+ continue;
+
+ try
+ {
+ path fcl (vd / cl);
+ dir_path cd (path_cast<dir_path> (followsymlink (fcl)));
+
+ if (cd.sub (br))
+ configs.push_back (
+ bindist_config {dd, rd, fcl.leaf (br), cd.leaf (br)});
+ }
+ catch (const system_error&) {}
+ }
+ }
+ catch (const system_error&) {}
+ }
+ }
+ catch (const system_error&) {}
+ }
+ }
+ catch (const system_error&) {}
+
+ // Sort and print collected package configurations, if any.
+ //
+ if (!configs.empty ())
+ {
+ sort (configs.begin (), configs.end ());
+
+ s << H3 << "Binaries" << ~H3
+ << TABLE(ID="binaries")
+ << TBODY;
+
+ for (const bindist_config& c: configs)
+ {
+ s << TR(CLASS="binaries")
+ << TD << SPAN(CLASS="value") << c.distribution << ~SPAN << ~TD
+ << TD << SPAN(CLASS="value") << c.os_release << ~SPAN << ~TD
+ << TD
+ << SPAN(CLASS="value")
+ << A
+ << HREF
+ << options_->bindist_url () << '/' << c.symlink
+ << ~HREF
+ << c.symlink.leaf ()
+ << ~A
+ << " ("
+ << A
+ << HREF
+ << options_->bindist_url () << '/' << c.directory
+ << ~HREF
+ << "snapshot"
+ << ~A
+ << ")"
+ << ~SPAN
+ << ~TD
+ << ~TR;
+ }
+
+ s << ~TBODY
+ << ~TABLE;
+ }
+ }
+
+ if (builds)
+ {
s << H3 << "Builds" << ~H3
<< DIV(ID="builds");
- auto exclude = [&pkg, this] (const build_config& cfg,
- string* reason = nullptr)
+ auto exclude = [&pkg, this] (const package_build_config& pc,
+ const build_target_config& tc,
+ string* rs = nullptr)
{
- return this->exclude (pkg->builds, pkg->build_constraints, cfg, reason);
+ return this->exclude (pc, pkg->builds, pkg->build_constraints, tc, rs);
};
timestamp now (system_clock::now ());
@@ -475,13 +742,7 @@ handle (request& rq, response& rs)
// Query toolchains seen for the package tenant to produce a list of the
// unbuilt configuration/toolchain combinations.
//
- // Note that it only make sense to print those unbuilt configurations that
- // may still be built. That's why we leave the toolchains list empty if
- // the package tenant is achieved.
- //
vector<pair<string, version>> toolchains;
-
- if (!archived)
{
using query = query<toolchain>;
@@ -492,49 +753,73 @@ handle (request& rq, response& rs)
"ORDER BY" + query::build::id.toolchain_name +
order_by_version_desc (query::build::id.toolchain_version,
false /* first */)))
+ {
toolchains.emplace_back (move (t.name), move (t.version));
+ }
}
- // Collect configuration names and unbuilt configurations, skipping those
- // that are hidden or excluded by the package.
+ // Compose the configuration filtering sub-query and collect unbuilt
+ // target configurations, skipping those that are hidden or excluded by
+ // the package configurations.
//
- cstrings conf_names;
+ using query = query<build>;
+
+ query sq (false);
set<config_toolchain> unbuilt_configs;
- for (const auto& c: *build_conf_map_)
+ for (const package_build_config& pc: pkg->build_configs)
{
- const build_config& cfg (*c.second);
-
- if (belongs (cfg, "all") && !exclude (cfg))
+ for (const auto& bc: *target_conf_map_)
{
- conf_names.push_back (c.first);
-
- // Note: we will erase built configurations from the unbuilt
- // configurations set later (see below).
- //
- for (const auto& t: toolchains)
- unbuilt_configs.insert ({cfg.name, t.first, t.second});
+ const build_target_config& tc (*bc.second);
+
+ if (!belongs (tc, "hidden") && !exclude (pc, tc))
+ {
+ const build_target_config_id& id (bc.first);
+
+ sq = sq || (query::id.target == id.target &&
+ query::id.target_config_name == id.config &&
+ query::id.package_config_name == pc.name);
+
+ // Note: we will erase built configurations from the unbuilt
+ // configurations set later (see below).
+ //
+ for (const auto& t: toolchains)
+ unbuilt_configs.insert (config_toolchain {tc.target,
+ tc.name,
+ pc.name,
+ t.first,
+ t.second});
+ }
}
}
- // Print the package built configurations in the time-descending order.
+ // Let's not print the package configuration row if the default
+ // configuration is the only one.
//
- using query = query<build>;
+ bool ppc (pkg->build_configs.size () != 1); // Note: can't be empty.
+ // Print the package built configurations in the time-descending order.
+ //
for (auto& b: build_db_->query<build> (
- (query::id.package == pkg->id &&
-
- query::id.configuration.in_range (conf_names.begin (),
- conf_names.end ())) +
-
+ (query::id.package == pkg->id && query::state != "queued" && sq) +
"ORDER BY" + query::timestamp + "DESC"))
{
string ts (butl::to_string (b.timestamp,
"%Y-%m-%d %H:%M:%S %Z",
true /* special */,
true /* local */) +
- " (" + butl::to_string (now - b.timestamp, false) + " ago)");
+ " (" + butl::to_string (now - b.timestamp, false) + " ago");
+
+ if (tn->archived)
+ ts += ", archived";
+
+ ts += ')';
+ // @@ Note that here we also load result logs which we don't need.
+ // Probably we should invent some table view to only load operation
+ // names and statuses.
+ //
if (b.state == build_state::built)
build_db_->load (b, b.results_section);
@@ -543,19 +828,29 @@ handle (request& rq, response& rs)
<< TR_VALUE ("toolchain",
b.toolchain_name + '-' +
b.toolchain_version.string ())
- << TR_VALUE ("config",
- b.configuration + " / " + b.target.string ())
- << TR_VALUE ("timestamp", ts)
- << TR_BUILD_RESULT (b, host, root)
+ << TR_VALUE ("target", b.target.string ())
+ << TR_VALUE ("tgt config", b.target_config_name);
+
+ if (ppc)
+ s << TR_VALUE ("pkg config", b.package_config_name);
+
+ s << TR_VALUE ("timestamp", ts);
+
+ if (b.interactive) // Note: can only be present for the building state.
+ s << TR_VALUE ("login", *b.interactive);
+
+ s << TR_BUILD_RESULT (b, tn->archived, host, root)
<< ~TBODY
<< ~TABLE;
// While at it, erase the built configuration from the unbuilt
// configurations set.
//
- unbuilt_configs.erase ({b.id.configuration,
- b.toolchain_name,
- b.toolchain_version});
+ unbuilt_configs.erase (config_toolchain {b.target,
+ b.target_config_name,
+ b.package_config_name,
+ b.toolchain_name,
+ b.toolchain_version});
}
// Print the package unbuilt configurations with the following sort
@@ -563,42 +858,57 @@ handle (request& rq, response& rs)
//
// 1: toolchain name
// 2: toolchain version (descending)
- // 3: configuration name
+ // 3: target
+ // 4: target configuration name
+ // 5: package configuration name
//
for (const auto& ct: unbuilt_configs)
{
- auto i (build_conf_map_->find (ct.configuration.c_str ()));
- assert (i != build_conf_map_->end ());
-
s << TABLE(CLASS="proplist build")
<< TBODY
<< TR_VALUE ("toolchain",
ct.toolchain_name + '-' +
ct.toolchain_version.string ())
- << TR_VALUE ("config",
- ct.configuration + " / " +
- i->second->target.string ())
- << TR_VALUE ("result", "unbuilt")
+ << TR_VALUE ("target", ct.target.string ())
+ << TR_VALUE ("tgt config", ct.target_config);
+
+ if (ppc)
+ s << TR_VALUE ("pkg config", ct.package_config);
+
+ s << TR_VALUE ("result", "unbuilt")
<< ~TBODY
<< ~TABLE;
}
- // Print the package build exclusions that belong to the 'default' class.
+ // Print the package build exclusions that belong to the 'default' class,
+ // unless the package is built interactively (normally for a single
+ // configuration).
//
- for (const auto& c: *build_conf_)
+ if (!tn->interactive)
{
- string reason;
- if (belongs (c, "default") && exclude (c, &reason))
+ for (const package_build_config& pc: pkg->build_configs)
{
- s << TABLE(CLASS="proplist build")
- << TBODY
- << TR_VALUE ("config", c.name + " / " + c.target.string ())
- << TR_VALUE ("result",
- !reason.empty ()
- ? "excluded (" + reason + ')'
- : "excluded")
- << ~TBODY
- << ~TABLE;
+ for (const auto& tc: *target_conf_)
+ {
+ string reason;
+ if (belongs (tc, "default") && exclude (pc, tc, &reason))
+ {
+ s << TABLE(CLASS="proplist build")
+ << TBODY
+ << TR_VALUE ("target", tc.target.string ())
+ << TR_VALUE ("tgt config", tc.name);
+
+ if (ppc)
+ s << TR_VALUE ("pkg config", pc.name);
+
+ s << TR_VALUE ("result",
+ !reason.empty ()
+ ? "excluded (" + reason + ')'
+ : "excluded")
+ << ~TBODY
+ << ~TABLE;
+ }
+ }
}
}
@@ -607,19 +917,25 @@ handle (request& rq, response& rs)
s << ~DIV;
}
- const string& ch (pkg->changes);
-
- if (!ch.empty ())
+ if (const optional<typed_text>& c = pkg->changes)
{
const string id ("changes");
+ const string what (title + " changes");
s << H3 << "Changes" << ~H3
<< (full
- ? PRE_TEXT (ch, id)
- : PRE_TEXT (ch,
+ ? DIV_TEXT (*c,
+ false /* strip_title */,
+ id,
+ what,
+ error)
+ : DIV_TEXT (*c,
+ false /* strip_title */,
options_->package_changes (),
- url (!full, "changes"),
- id));
+ url (!full, id),
+ id,
+ what,
+ error));
}
s << ~DIV