aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--bpkg/bpkg-options.cli14
-rw-r--r--bpkg/bpkg.cxx26
-rw-r--r--bpkg/buildfile5
-rw-r--r--bpkg/cfg-create.cxx1
-rw-r--r--bpkg/database.cxx4
-rwxr-xr-xbpkg/odb.sh6
-rw-r--r--bpkg/package20
-rw-r--r--bpkg/package.xml2
-rw-r--r--bpkg/pkg-fetch17
-rw-r--r--bpkg/pkg-fetch-options.cli44
-rw-r--r--bpkg/pkg-fetch.cxx114
-rw-r--r--bpkg/pkg-verify27
-rw-r--r--bpkg/pkg-verify-options.cli34
-rw-r--r--bpkg/pkg-verify.cxx169
-rw-r--r--bpkg/rep-create.cxx114
-rw-r--r--bpkg/types3
-rw-r--r--bpkg/wrapper-traits59
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;
}
}
}
diff --git a/bpkg/types b/bpkg/types
index 2ab7aea..4110ed8 100644
--- a/bpkg/types
+++ b/bpkg/types
@@ -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