// 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> #include <libstudxml/serializer.hxx> #include <odb/session.hxx> #include <odb/database.hxx> #include <odb/transaction.hxx> #include <web/xhtml.hxx> #include <web/module.hxx> #include <web/mime-url-encoding.hxx> #include <libbrep/build.hxx> #include <libbrep/build-odb.hxx> #include <libbrep/package.hxx> #include <libbrep/package-odb.hxx> #include <mod/page.hxx> #include <mod/options.hxx> using namespace std; using namespace butl; using namespace odb::core; using namespace brep::cli; // While currently the user-defined copy constructor is not required (we don't // need to deep copy nullptr's), it is a good idea to keep the placeholder // ready for less trivial cases. // brep::package_version_details:: package_version_details (const package_version_details& r) : database_module (r), build_config_module (r), options_ (r.initialized_ ? r.options_ : nullptr) { } void brep::package_version_details:: init (scanner& s) { HANDLER_DIAG; options_ = make_shared<options::package_version_details> ( s, unknown_mode::fail, unknown_mode::fail); database_module::init (static_cast<options::package_db> (*options_), options_->package_db_retry ()); if (options_->build_config_specified ()) { database_module::init (static_cast<options::build_db> (*options_), options_->build_db_retry ()); build_config_module::init (static_cast<options::build> (*options_)); } if (options_->root ().empty ()) options_->root (dir_path ("/")); } bool brep::package_version_details:: handle (request& rq, response& rs) { using namespace web; using namespace web::xhtml; using brep::version; // Not to confuse with module::version. HANDLER_DIAG; const string& host (options_->host ()); const dir_path& root (options_->root ()); auto i (rq.path ().rbegin ()); version ver; try { ver = version (*i++); } catch (const invalid_argument& ) { throw invalid_request (400, "invalid package version format"); } assert (i != rq.path ().rend ()); package_name pn; try { pn = package_name (*i); } catch (const invalid_argument& ) { throw invalid_request (400, "invalid package name format"); } params::package_version_details params; bool full; try { name_value_scanner s (rq.parameters (1024)); params = params::package_version_details ( s, unknown_mode::fail, unknown_mode::fail); full = params.form () == page_form::full; } catch (const cli::exception& e) { throw invalid_request (400, e.what ()); } const string& sver (ver.string ()); auto url = [&sver] (bool f = false, const string& a = "") -> string { string u (sver); if (f) { u += "?f=full"; } if (!a.empty ()) { u += '#' + a; } return u; }; bool not_found (false); shared_ptr<package> pkg; session sn; transaction t (package_db_->begin ()); try { pkg = package_db_->load<package> (package_id (tenant, pn, ver)); // If the requested package turned up to be an "external" one just // respond that no "internal" package is present. // not_found = !pkg->internal (); } catch (const object_not_persistent& ) { not_found = true; } if (not_found) throw invalid_request ( 404, "Package " + pn.string () + '/' + sver + " not (yet) found"); const string& name (pkg->name.string ()); const string title (name + " " + sver); xml::serializer s (rs.content (), title); s << HTML << HEAD << TITLE << title << ~TITLE << CSS_LINKS (path ("package-version-details.css"), root) << ~HEAD << BODY << DIV_HEADER (options_->logo (), options_->menu (), root, tenant) << DIV(ID="content"); if (full) s << CLASS("full"); s << DIV(ID="heading") << H1 << A(HREF=tenant_dir (root, tenant) / path (mime_url_encode (name, false))) << name << ~A << "/" << A(HREF=url ()) << sver << ~A << ~H1 << A(HREF=url (!full)) << (full ? "[brief]" : "[full]") << ~A << ~DIV; s << H2 << pkg->summary << ~H2; if (const optional<string>& d = pkg->description) { const string id ("description"); const string what (title + " description"); s << (full ? DIV_TEXT (*d, * pkg->description_type, true /* strip_title */, id, what, error) : DIV_TEXT (*d, *pkg->description_type, true /* strip_title */, options_->package_description (), url (!full, id), id, what, error)); } const repository_location& rl (pkg->internal_repository.load ()->location); s << TABLE(CLASS="proplist", ID="version") << TBODY // Repeat version here since it can be cut out in the header. // << TR_VERSION (pkg->version, pkg->upstream_version) << TR_PRIORITY (pkg->priority) << TR_LICENSES (pkg->license_alternatives) << TR_REPOSITORY (rl.canonical_name (), root, tenant) << TR_LOCATION (rl); if (rl.type () == repository_type::pkg) { assert (pkg->location); s << TR_LINK (rl.url ().string () + "/" + pkg->location->string (), pkg->location->leaf ().string (), "download"); } if (pkg->fragment) s << TR_VALUE ("fragment", *pkg->fragment); if (pkg->sha256sum) s << TR_SHA256SUM (*pkg->sha256sum); s << ~TBODY << ~TABLE << TABLE(CLASS="proplist", ID="package") << TBODY << TR_PROJECT (pkg->project, root, tenant); const auto& u (pkg->url); if (u) s << TR_URL (*u); if (pkg->doc_url) s << TR_URL (*pkg->doc_url, "doc-url"); if (pkg->src_url) s << TR_URL (*pkg->src_url, "src-url"); const auto& pu (pkg->package_url); if (pu && pu != u) s << TR_URL (*pu, "package-url"); const auto& em (pkg->email); if (em) s << TR_EMAIL (*em); const auto& pe (pkg->package_email); if (pe && pe != em) s << TR_EMAIL (*pe, "package-email"); s << TR_TOPICS (pkg->topics, root, tenant) << ~TBODY << ~TABLE; const auto& ds (pkg->dependencies); if (!ds.empty ()) { s << H3 << "Depends (" << ds.size () << ")" << ~H3 << TABLE(CLASS="proplist", ID="depends") << TBODY; for (const auto& da: ds) { s << TR(CLASS="depends") << TH; if (da.conditional) s << "?"; if (da.buildtime) s << "*"; s << ~TH << TD << SPAN(CLASS="value"); for (const auto& d: da) { if (&d != &da[0]) s << " | "; const auto& dcon (d.constraint); const package_name& dname (d.name); // Try to display the dependency as a link if it is resolved. // Otherwise display it as a plain text. // if (d.package != nullptr) { shared_ptr<package> p (d.package.load ()); assert (p->internal () || !p->other_repositories.empty ()); shared_ptr<repository> r ( p->internal () ? p->internal_repository.load () : p->other_repositories[0].load ()); string ename (mime_url_encode (dname.string (), false)); if (r->interface_url) { string u (*r->interface_url + ename); s << A(HREF=u) << dname << ~A; if (dcon) s << ' ' << A(HREF=u + "/" + p->version.string ()) << *dcon << ~A; } else if (p->internal ()) { dir_path u (tenant_dir (root, tenant) / dir_path (ename)); s << A(HREF=u) << dname << ~A; if (dcon) s << ' ' << A(HREF=u / path (p->version.string ())) << *dcon << ~A; } else // Display the dependency as a plain text if no repository URL // available. // s << d; } else s << d; } s << ~SPAN << SPAN_COMMENT (da.comment) << ~TD << ~TR; } s << ~TBODY << ~TABLE; } // Don't display the page builds section for stub packages. // bool builds (build_db_ != nullptr && ver.compare (wildcard_version, true) != 0); if (builds) package_db_->load (*pkg, pkg->build_section); t.commit (); const auto& rm (pkg->requirements); if (!rm.empty ()) { s << H3 << "Requires (" << rm.size () << ")" << ~H3 << TABLE(CLASS="proplist", ID="requires") << TBODY; for (const auto& ra: rm) { s << TR(CLASS="requires") << TH; if (ra.conditional) s << "?"; if (ra.buildtime) s << "*"; if (ra.conditional || ra.buildtime) s << " "; s << ~TH << TD << SPAN(CLASS="value"); for (const auto& r: ra) { if (&r != &ra[0]) s << " | "; s << r; } s << ~SPAN << SPAN_COMMENT (ra.comment) << ~TD << ~TR; } s << ~TBODY << ~TABLE; } if (builds) { using bbot::build_config; s << H3 << "Builds" << ~H3 << DIV(ID="builds"); auto exclude = [&pkg, this] (const build_config& cfg, string* reason = nullptr) { return this->exclude (pkg->builds, pkg->build_constraints, cfg, reason); }; timestamp now (system_clock::now ()); transaction t (build_db_->begin ()); // Print built package configurations, except those that are hidden or // excluded by the package. // cstrings conf_names; for (const auto& c: *build_conf_map_) { if (belongs (*c.second, "all")) conf_names.push_back (c.first); } using query = query<build>; for (auto& b: build_db_->query<build> ( (query::id.package == pkg->id && query::id.configuration.in_range (conf_names.begin (), conf_names.end ())) + "ORDER BY" + query::timestamp + "DESC")) { auto i (build_conf_map_->find (b.configuration.c_str ())); assert (i != build_conf_map_->end ()); const build_config& cfg (*i->second); if (exclude (cfg)) continue; string ts (butl::to_string (b.timestamp, "%Y-%m-%d %H:%M:%S %Z", true, true) + " (" + butl::to_string (now - b.timestamp, false) + " ago)"); if (b.state == build_state::built) build_db_->load (b, b.results_section); s << TABLE(CLASS="proplist build") << TBODY << 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) << ~TBODY << ~TABLE; } // Print the package build exclusions that belong to the 'default' class. // for (const auto& c: *build_conf_) { string reason; if (belongs (c, "default") && exclude (c, &reason)) { s << TABLE(CLASS="proplist build") << TBODY << TR_VALUE ("config", c.name + " / " + c.target.string ()) << TR_VALUE ("result", !reason.empty () ? "excluded (" + reason + ')' : "excluded") << ~TBODY << ~TABLE; } } t.commit (); s << ~DIV; } const string& ch (pkg->changes); if (!ch.empty ()) { const string id ("changes"); s << H3 << "Changes" << ~H3 << (full ? PRE_TEXT (ch, id) : PRE_TEXT (ch, options_->package_changes (), url (!full, "changes"), id)); } s << ~DIV << ~BODY << ~HTML; return true; }