diff options
author | Boris Kolpackov <boris@codesynthesis.com> | 2015-09-15 10:58:17 +0200 |
---|---|---|
committer | Boris Kolpackov <boris@codesynthesis.com> | 2015-09-15 10:58:17 +0200 |
commit | 333c5953151d6324d83d279a7ac3c53cd1af54b9 (patch) | |
tree | e473c5ca4461afcbdf67d5db42e883a1d76564e9 | |
parent | d05f9f046565f2d0d4135912103f96f0e66b454f (diff) |
Implement pkg-verify, pkg-fetch commands
-rw-r--r-- | bpkg/bpkg-options.cli | 14 | ||||
-rw-r--r-- | bpkg/bpkg.cxx | 26 | ||||
-rw-r--r-- | bpkg/buildfile | 5 | ||||
-rw-r--r-- | bpkg/cfg-create.cxx | 1 | ||||
-rw-r--r-- | bpkg/database.cxx | 4 | ||||
-rwxr-xr-x | bpkg/odb.sh | 6 | ||||
-rw-r--r-- | bpkg/package | 20 | ||||
-rw-r--r-- | bpkg/package.xml | 2 | ||||
-rw-r--r-- | bpkg/pkg-fetch | 17 | ||||
-rw-r--r-- | bpkg/pkg-fetch-options.cli | 44 | ||||
-rw-r--r-- | bpkg/pkg-fetch.cxx | 114 | ||||
-rw-r--r-- | bpkg/pkg-verify | 27 | ||||
-rw-r--r-- | bpkg/pkg-verify-options.cli | 34 | ||||
-rw-r--r-- | bpkg/pkg-verify.cxx | 169 | ||||
-rw-r--r-- | bpkg/rep-create.cxx | 114 | ||||
-rw-r--r-- | bpkg/types | 3 | ||||
-rw-r--r-- | bpkg/wrapper-traits | 59 |
17 files changed, 559 insertions, 100 deletions
diff --git a/bpkg/bpkg-options.cli b/bpkg/bpkg-options.cli index d8dc945..c3bfd24 100644 --- a/bpkg/bpkg-options.cli +++ b/bpkg/bpkg-options.cli @@ -21,6 +21,20 @@ namespace bpkg "" }; + bool pkg-verify + { + "<archive>", + "Verify archive is a valid \cb{bpkg} package.", + "" + }; + + bool pkg-fetch + { + "<pkg> <ver>", + "Fetch package archive.", + "" + }; + bool cfg-create { "[<conf>]", diff --git a/bpkg/bpkg.cxx b/bpkg/bpkg.cxx index eeafa09..d1ee945 100644 --- a/bpkg/bpkg.cxx +++ b/bpkg/bpkg.cxx @@ -14,6 +14,8 @@ // Commands. // #include <bpkg/help> +#include <bpkg/pkg-verify> +#include <bpkg/pkg-fetch> #include <bpkg/cfg-create> #include <bpkg/rep-create> @@ -135,6 +137,30 @@ try return 0; } + // pkg-verify + // + if (cmd.pkg_verify ()) + { + if (h) + help (ho, "pkg-verify", pkg_verify_options::print_usage); + else + pkg_verify (parse<pkg_verify_options> (co, args), args); + + return 0; + } + + // pkg-fetch + // + if (cmd.pkg_fetch ()) + { + if (h) + help (ho, "pkg-fetch", pkg_fetch_options::print_usage); + else + pkg_fetch (parse<pkg_fetch_options> (co, args), args); + + return 0; + } + // cfg-create // if (cmd.cfg_create ()) diff --git a/bpkg/buildfile b/bpkg/buildfile index b393780..0d00a9f 100644 --- a/bpkg/buildfile +++ b/bpkg/buildfile @@ -13,6 +13,8 @@ exe{bpkg}: cxx{package package-odb database diagnostics utility} \ cli.cxx{common-options} cxx{types-parsers} \ cxx{bpkg} cli.cxx{bpkg-options} \ cxx{help} cli.cxx{help-options} \ + cxx{pkg-verify} cli.cxx{pkg-verify-options} \ + cxx{pkg-fetch} cli.cxx{pkg-fetch-options} \ cxx{cfg-create} cli.cxx{cfg-create-options} \ cxx{rep-create} cli.cxx{rep-create-options} \ $libs @@ -32,5 +34,8 @@ cli.cxx{bpkg-options}: cli{bpkg-options} cli.cxx{bpkg-options}: cli.options += --option-length 22 --short-usage cli.cxx{help-options}: cli{help-options} + +cli.cxx{pkg-verify-options}: cli{pkg-verify-options} +cli.cxx{pkg-fetch-options}: cli{pkg-fetch-options} cli.cxx{cfg-create-options}: cli{cfg-create-options} cli.cxx{rep-create-options}: cli{rep-create-options} diff --git a/bpkg/cfg-create.cxx b/bpkg/cfg-create.cxx index 08e64b1..2fa001c 100644 --- a/bpkg/cfg-create.cxx +++ b/bpkg/cfg-create.cxx @@ -7,7 +7,6 @@ #include <utility> // move() #include <cassert> #include <fstream> -#include <iostream> #include <bpkg/types> #include <bpkg/utility> diff --git a/bpkg/database.cxx b/bpkg/database.cxx index 01488c6..3488fa1 100644 --- a/bpkg/database.cxx +++ b/bpkg/database.cxx @@ -11,6 +11,7 @@ #include <odb/sqlite/exceptions.hxx> #include <bpkg/types> +#include <bpkg/utility> #include <bpkg/diagnostics> using namespace std; @@ -25,6 +26,9 @@ namespace bpkg { path f (d / path ("bpkg.sqlite3")); + if (!create && !exists (f)) + fail << d << " does not look like a bpkg configuration directory"; + try { // We don't need the thread pool. diff --git a/bpkg/odb.sh b/bpkg/odb.sh index 12ac408..330218a 100755 --- a/bpkg/odb.sh +++ b/bpkg/odb.sh @@ -9,7 +9,9 @@ lib="\ -I/home/boris/work/odb/libodb-default \ -I/home/boris/work/odb/libodb" -$odb -d sqlite --std c++11 --hxx-suffix "" --generate-query --generate-schema \ - $lib -I.. -I../../libbpkg -I../../libbutl \ +$odb $lib -I.. -I../../libbpkg -I../../libbutl \ + -d sqlite --std c++11 --hxx-suffix "" --generate-query --generate-schema \ + --odb-epilogue '#include <bpkg/wrapper-traits>' \ + --hxx-prologue '#include <bpkg/wrapper-traits>' \ --include-with-brackets --include-prefix bpkg --guard-prefix BPKG \ --sqlite-override-null package diff --git a/bpkg/package b/bpkg/package index 2aa7412..8c1e047 100644 --- a/bpkg/package +++ b/bpkg/package @@ -35,6 +35,18 @@ namespace bpkg namespace bpkg { + // path + // + #pragma db map type(path) as(string) \ + to((?).string ()) from(bpkg::path (?)) + + using optional_path = optional<path>; + using optional_string = optional<string>; + + #pragma db map type(optional_path) as(bpkg::optional_string) \ + to((?) ? (?)->string () : bpkg::optional_string ()) \ + from((?) ? bpkg::path (*(?)) : bpkg::optional_path ()) + // version // #pragma db map type(version) as(_version) \ @@ -81,6 +93,14 @@ namespace bpkg version_type version; state_type state; + // Path to the archive of this package, if any. If not absolute, + // then it is relative to the configuration directory. The purge + // flag indicates whether the archive should be removed when the + // packaged is purged. + // + optional<path> archive; + bool archive_purge; + // Database mapping. // #pragma db member(name) id diff --git a/bpkg/package.xml b/bpkg/package.xml index dc5f197..22f71e7 100644 --- a/bpkg/package.xml +++ b/bpkg/package.xml @@ -7,6 +7,8 @@ <column name="version_revision" type="INTEGER" null="true"/> <column name="version_canonical_upstream" type="TEXT" null="true"/> <column name="state" type="TEXT" null="true"/> + <column name="archive" type="TEXT" null="true"/> + <column name="archive_purge" type="INTEGER" null="true"/> <primary-key> <column name="name"/> </primary-key> diff --git a/bpkg/pkg-fetch b/bpkg/pkg-fetch new file mode 100644 index 0000000..1f25c82 --- /dev/null +++ b/bpkg/pkg-fetch @@ -0,0 +1,17 @@ +// file : bpkg/pkg-fetch -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BPKG_PKG_FETCH +#define BPKG_PKG_FETCH + +#include <bpkg/types> +#include <bpkg/pkg-fetch-options> + +namespace bpkg +{ + void + pkg_fetch (const pkg_fetch_options&, cli::scanner& args); +} + +#endif // BPKG_PKG_FETCH diff --git a/bpkg/pkg-fetch-options.cli b/bpkg/pkg-fetch-options.cli new file mode 100644 index 0000000..6f7386b --- /dev/null +++ b/bpkg/pkg-fetch-options.cli @@ -0,0 +1,44 @@ +// file : bpkg/pkg-fetch-options.cli +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +include <bpkg/common-options.cli>; + +/* +"\section=1" +"\name=bpkg-pkg-fetch" + +"\h{SYNOPSIS} + +bpkg pkg-fetch [<options>] (<pkg> <ver>)|(-a <archive>)" + +"\h{DESCRIPTION} + +The \cb{pkg-fetch} command fetches the archive for the specified package +name and version from one of the configuration's repositories. If the +\cb{-a|--archive} option is used, then instead of the name and version +arguments, \cb{pkg-fetch} expects a local path to the package archive +file. In this case, \cb{bpkg} will use the archive in place, without +copying it to the configuration or package cache directories. It will +also not attempt to remove the archive if the package is purged with +the \cb{pkg-purge} command." +*/ + +namespace bpkg +{ + class pkg_fetch_options: common_options + { + dir_path --directory | -d (".") + { + "<dir>", + "Assume configuration is in <dir> rather than in the current working + directory." + }; + + bool --archive | -a + { + "Treat the argument as a package archive path rather than package + name/version to fetch." + }; + }; +} diff --git a/bpkg/pkg-fetch.cxx b/bpkg/pkg-fetch.cxx new file mode 100644 index 0000000..89283c6 --- /dev/null +++ b/bpkg/pkg-fetch.cxx @@ -0,0 +1,114 @@ +// file : bpkg/pkg-fetch.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <bpkg/pkg-fetch> + +#include <memory> // shared_ptr + +#include <bpkg/manifest> + +#include <bpkg/types> +#include <bpkg/package> +#include <bpkg/package-odb> +#include <bpkg/utility> +#include <bpkg/database> +#include <bpkg/pkg-verify> +#include <bpkg/diagnostics> + +using namespace std; +using namespace butl; + +namespace bpkg +{ + void + pkg_fetch (const pkg_fetch_options& o, cli::scanner& args) + { + tracer trace ("pkg_fetch"); + + dir_path d (o.directory ()); + level4 ([&]{trace << "configuration: " << d;}); + + database db (open (d)); + + path a; + bool purge; + + if (o.archive ()) + { + if (!args.more ()) + fail << "archive path argument expected" << + info << "run 'bpkg help pkg-fetch' for more information"; + + a = path (args.next ()); + + if (!exists (a)) + fail << "archive file '" << a << "' does not exist"; + + purge = false; + } + else + { + if (!args.more ()) + fail << "package name argument expected" << + info << "run 'bpkg help pkg-fetch' for more information"; + + string name (args.next ()); + + if (!args.more ()) + fail << "package version argument expected" << + info << "run 'bpkg help pkg-fetch' for more information"; + + string ver (args.next ()); + + // TODO: + // + // - Will need to use some kind or auto_rm/exception guard on + // fetched file. + // + + fail << "pkg-fetch " << name << " " << ver << " not yet implemented"; + purge = true; + } + + level4 ([&]{trace << "package archive: " << a << ", purge: " << purge;}); + + // Verify archive is a package and get its manifest. + // + package_manifest m (pkg_verify (a)); + level4 ([&]{trace << m.name << " " << m.version;}); + + const auto& n (m.name); + + // Database time. + // + transaction t (db.begin ()); + + // See if this package already exists in this configuration. + // + if (shared_ptr<package> p = db.find<package> (n)) + fail << "package " << n << " already exists in configuration " << d << + info << "existing version: " << p->version << ", state: " << p->state; + + // Make the archive and configuration paths absolute and normalized. + // If the archive is inside the configuration, use the relative path. + // This way we can move configuration around. + // + d.complete ().normalize (); + a.complete ().normalize (); + + if (a.sub (d)) + a = a.leaf (d); + + // Add the package to the configuration. + // + package p {move (m.name), + move (m.version), + state::fetched, + move (a), + purge}; + + db.persist (p); + t.commit (); + } +} diff --git a/bpkg/pkg-verify b/bpkg/pkg-verify new file mode 100644 index 0000000..e002bf3 --- /dev/null +++ b/bpkg/pkg-verify @@ -0,0 +1,27 @@ +// file : bpkg/pkg-verify -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BPKG_PKG_VERIFY +#define BPKG_PKG_VERIFY + +#include <bpkg/manifest> + +#include <bpkg/types> +#include <bpkg/pkg-verify-options> + +namespace bpkg +{ + void + pkg_verify (const pkg_verify_options&, cli::scanner& args); + + // Verify archive is a valid package and return its manifest. Throw + // failed if invalid or if something goes wrong. If diag is false, + // then don't issue diagnostics about the reason why the package is + // invalid. + // + package_manifest + pkg_verify (const path& archive, bool diag = true); +} + +#endif // BPKG_PKG_VERIFY diff --git a/bpkg/pkg-verify-options.cli b/bpkg/pkg-verify-options.cli new file mode 100644 index 0000000..cffdb00 --- /dev/null +++ b/bpkg/pkg-verify-options.cli @@ -0,0 +1,34 @@ +// file : bpkg/pkg-verify-options.cli +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +include <bpkg/common-options.cli>; + +/* +"\section=1" +"\name=bpkg-pkg-verify" + +"\h{SYNOPSIS} + +bpkg pkg-verify <archive>" + +"\h{DESCRIPTION} + +The \cb{pkg-verify} command verifies that the specified archive is a +valid \cb{bpkg} package. Specifically, it checks that the archive +contains a valid manifest file and that both the archive's name +and its top-level directory match the canonical <name>-<version> +form." +*/ + +namespace bpkg +{ + class pkg_verify_options: common_options + { + bool --silent + { + "Suppress error messages about the reason why the package is + invalid. Just return the error status." + }; + }; +} diff --git a/bpkg/pkg-verify.cxx b/bpkg/pkg-verify.cxx new file mode 100644 index 0000000..a1aa64b --- /dev/null +++ b/bpkg/pkg-verify.cxx @@ -0,0 +1,169 @@ +// file : bpkg/pkg-verify.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <bpkg/pkg-verify> + +#include <butl/process> +#include <butl/fdstream> + +#include <bpkg/manifest-parser> + +#include <bpkg/types> +#include <bpkg/utility> +#include <bpkg/diagnostics> + +using namespace std; +using namespace butl; + +namespace bpkg +{ + package_manifest + pkg_verify (const path& af, bool diag) + { + // Figure out the package directory. Strip the top-level extension + // and, as a special case, if the second-level extension is .tar, + // strip that as well (e.g., .tar.bz2). + // + path pd (af.leaf ().base ()); + if (const char* e = pd.extension ()) + { + if (e == string ("tar")) + pd = pd.base (); + } + + // Extract the manifest. + // + path mf (pd / path ("manifest")); + + const char* args[] { + "tar", + "-xOf", // -O/--to-stdout -- extract to STDOUT. + af.string ().c_str (), + mf.string ().c_str (), + nullptr}; + + if (verb >= 2) + print_process (args); + + try + { + // If diag is false, we need to make tar not print any diagnostics. + // There doesn't seem to be an option to suppress this and the only + // way is to redirect STDERR to something like /dev/null. To keep + // things simple, we are going to redirect it to STDOUT, which we + // in turn redirect to a pipe and use to parse the manifest data. + // If things go badly for tar and it starts spitting errors instead + // of the manifest, the manifest parser will fail. But that's ok + // since we assume that the child error is always the reason for + // the manifest parsing failure. + // + process pr (args, 0, -1, (diag ? 2 : 1)); + + try + { + ifdstream is (pr.in_ofd); + is.exceptions (ifdstream::badbit | ifdstream::failbit); + + manifest_parser mp (is, mf.string ()); + package_manifest m (mp); + is.close (); + + if (pr.wait ()) + { + // Verify package archive/directory is <name>-<version>. + // + path ed (m.name + "-" + m.version.string ()); + + if (pd != ed) + { + if (diag) + error << "package archive/directory name mismatch in " << af << + info << "extracted from archive '" << pd << "'" << + info << "expected from manifest '" << ed << "'"; + + throw failed (); + } + + return m; + } + + // Child existed with an error, fall through. + } + // Ignore these exceptions if the child process exited with + // an error status since that's the source of the failure. + // + catch (const manifest_parsing& e) + { + if (pr.wait ()) + { + if (diag) + error (e.name, e.line, e.column) << e.description << + info << "package archive " << af; + + throw failed (); + } + } + catch (const ifdstream::failure&) + { + if (pr.wait ()) + { + if (diag) + error << "unable to extract " << mf << " from " << af; + + throw failed (); + } + } + + // We should only get here if the child exited with an error + // status. + // + assert (!pr.wait ()); + + // While it is reasonable to assuming the child process issued + // diagnostics, tar, specifically, doesn't mention the archive + // name. + // + if (diag) + error << af << " does not appear to be a bpkg package"; + + throw failed (); + } + catch (const process_error& e) + { + // Note: this is not an "invalid package" case, so no diag check. + // + error << "unable to execute " << args[0] << ": " << e.what (); + + if (e.child ()) + exit (1); + + throw failed (); + } + } + + void + pkg_verify (const pkg_verify_options& o, cli::scanner& args) + { + tracer trace ("pkg_verify"); + + if (!args.more ()) + fail << "archive path argument expected" << + info << "run 'bpkg help pkg-verify' for more information"; + + path a (args.next ()); + + if (!exists (a)) + fail << "archive file '" << a << "' does not exist"; + + level4 ([&]{trace << "archive: " << a;}); + + // If we were asked to run silent, don't yap about the reason + // why the package is invalid. Just return the error status. + // + package_manifest m (pkg_verify (a, !o.silent ())); + + if (verb && !o.silent ()) + text << "valid package " << m.name << " " << m.version; + } +} diff --git a/bpkg/rep-create.cxx b/bpkg/rep-create.cxx index 42e7d55..9ea24c7 100644 --- a/bpkg/rep-create.cxx +++ b/bpkg/rep-create.cxx @@ -11,9 +11,7 @@ #include <iostream> #include <system_error> -#include <butl/process> -#include <butl/fdstream> -#include <butl/filesystem> +#include <butl/filesystem> // dir_iterator #include <bpkg/manifest> #include <bpkg/manifest-parser> @@ -21,6 +19,7 @@ #include <bpkg/types> #include <bpkg/utility> +#include <bpkg/pkg-verify> #include <bpkg/diagnostics> using namespace std; @@ -72,108 +71,29 @@ namespace bpkg continue; } - path fp (d / p); - - // Figure out the package directory. Strip the top-level extension - // and, as a special case, if the second-level extension is .tar, - // strip that as well (e.g., .tar.bz2). + // Verify archive is a package and get its manifest. // - p = p.base (); - if (const char* e = p.extension ()) - { - if (e == string ("tar")) - p = p.base (); - } + path a (d / p); + package_manifest m (pkg_verify (a)); - level4 ([&]{trace << "found package " << p << " in " << fp;}); + level4 ([&]{trace << m.name << " " << m.version << " in " << a;}); - // Extract the manifest. + // Add package archive location relative to the repository root. // - path mf (p / path ("manifest")); - - const char* args[] { - "tar", - "-xOf", // -O/--to-stdout -- extract to STDOUT. - fp.string ().c_str (), - mf.string ().c_str (), - nullptr}; - - if (verb >= 2) - print_process (args); + m.location = a.leaf (root); - try - { - process pr (args, 0, -1); // Open pipe to stdout. - - try - { - ifdstream is (pr.in_ofd); - is.exceptions (ifdstream::badbit | ifdstream::failbit); - - manifest_parser mp (is, mf.string ()); - package_manifest m (mp); - - // Verify package archive/directory is <name>-<version>. - // - { - path ep (m.name + "-" + m.version.string ()); - - if (p != ep) - fail << "package archive/directory name mismatch in " << fp << - info << "extracted from archive '" << p << "'" << - info << "expected from manifest '" << ep << "'"; - } - - // Add package archive location relative to the repository root. - // - m.location = fp.leaf (root); - - package_key k (m.name, m.version); // Argument evaluation order. - auto r (map.emplace (move (k), package_data (fp, move (m)))); - - // Diagnose duplicates. - // - if (!r.second) - { - const package_manifest& m (r.first->second.second); - - fail << "duplicate package " << m.name << " " << m.version << - info << "first package archive is " << r.first->second.first << - info << "second package archive is " << fp; - } - } - // Ignore these exceptions if the child process exited with - // an error status since that's the source of the failure. - // - catch (const manifest_parsing& e) - { - if (pr.wait ()) - fail (e.name, e.line, e.column) << e.description << - info << "package archive " << fp; - } - catch (const ifdstream::failure&) - { - if (pr.wait ()) - fail << "unable to extract " << mf << " from " << fp; - } + package_key k (m.name, m.version); // Argument evaluation order. + auto r (map.emplace (move (k), package_data (a, move (m)))); - if (!pr.wait ()) - { - // While it is reasonable to assuming the child process issued - // diagnostics, tar, specifically, doesn't mention the archive - // name. - // - fail << fp << " does not appear to be a bpkg package"; - } - } - catch (const process_error& e) + // Diagnose duplicates. + // + if (!r.second) { - error << "unable to execute " << args[0] << ": " << e.what (); - - if (e.child ()) - exit (1); + const package_manifest& m (r.first->second.second); - throw failed (); + fail << "duplicate package " << m.name << " " << m.version << + info << "first package archive is " << r.first->second.first << + info << "second package archive is " << a; } } } @@ -10,6 +10,7 @@ #include <ostream> #include <butl/path> +#include <butl/optional> namespace bpkg { @@ -20,6 +21,8 @@ namespace bpkg using strings = std::vector<string>; using cstrings = std::vector<const char*>; + using butl::optional; + // <butl/path> // using butl::path; diff --git a/bpkg/wrapper-traits b/bpkg/wrapper-traits new file mode 100644 index 0000000..add28e8 --- /dev/null +++ b/bpkg/wrapper-traits @@ -0,0 +1,59 @@ +// file : bpkg/wrapper-traits -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BPKG_WRAPPER_TRAITS +#define BPKG_WRAPPER_TRAITS + +#include <butl/optional> + +#include <odb/wrapper-traits.hxx> + +namespace odb +{ + template <typename T> + class wrapper_traits<butl::optional<T>> + { + public: + typedef T wrapped_type; + typedef butl::optional<T> wrapper_type; + + // T can be const. + // + typedef + typename odb::details::meta::remove_const<T>::result + unrestricted_wrapped_type; + + static const bool null_handler = true; + static const bool null_default = true; + + static bool + get_null (const wrapper_type& o) + { + return !o; + } + + static void + set_null (wrapper_type& o) + { + o = wrapper_type (); + } + + static const wrapped_type& + get_ref (const wrapper_type& o) + { + return *o; + } + + static unrestricted_wrapped_type& + set_ref (wrapper_type& o) + { + if (!o) + o = unrestricted_wrapped_type (); + + return const_cast<unrestricted_wrapped_type&> (*o); + } + }; +} + +#endif // BPKG_WRAPPER_TRAITS |