diff options
Diffstat (limited to 'build2/version')
-rw-r--r-- | build2/version/init | 31 | ||||
-rw-r--r-- | build2/version/init.cxx | 295 | ||||
-rw-r--r-- | build2/version/module | 32 | ||||
-rw-r--r-- | build2/version/module.cxx | 15 | ||||
-rw-r--r-- | build2/version/rule | 36 | ||||
-rw-r--r-- | build2/version/rule.cxx | 151 | ||||
-rw-r--r-- | build2/version/snapshot | 33 | ||||
-rw-r--r-- | build2/version/snapshot-git.cxx | 106 | ||||
-rw-r--r-- | build2/version/snapshot.cxx | 31 |
9 files changed, 730 insertions, 0 deletions
diff --git a/build2/version/init b/build2/version/init new file mode 100644 index 0000000..8ffb999 --- /dev/null +++ b/build2/version/init @@ -0,0 +1,31 @@ +// file : build2/version/init -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_VERSION_INIT +#define BUILD2_VERSION_INIT + +#include <build2/types> +#include <build2/utility> + +#include <build2/module> + +namespace build2 +{ + namespace version + { + void + boot (scope&, const location&, unique_ptr<module_base>&); + + bool + init (scope&, + scope&, + const location&, + unique_ptr<module_base>&, + bool, + bool, + const variable_map&); + } +} + +#endif // BUILD2_VERSION_INIT diff --git a/build2/version/init.cxx b/build2/version/init.cxx new file mode 100644 index 0000000..947f4a8 --- /dev/null +++ b/build2/version/init.cxx @@ -0,0 +1,295 @@ +// file : build2/version/init.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <build2/version/init> + +#include <butl/manifest-parser> +#include <butl/manifest-serializer> + +#include <build2/scope> +#include <build2/context> +#include <build2/variable> +#include <build2/diagnostics> + +#include <build2/dist/module> + +#include <build2/version/rule> +#include <build2/version/module> +#include <build2/version/snapshot> + +using namespace std; +using namespace butl; + +namespace build2 +{ + namespace version + { + static const path manifest ("manifest"); + + static const version_doc version_doc_; + + void + boot (scope& rs, const location& l, unique_ptr<module_base>& mod) + { + tracer trace ("version::boot"); + l5 ([&]{trace << "for " << rs.out_path ();}); + + // Extract the version from the manifest file. + // + standard_version v; + { + path f (rs.src_path () / manifest); + + try + { + if (!file_exists (f)) + fail (l) << "no manifest file in " << rs.src_path (); + + ifdstream ifs (f); + manifest_parser p (ifs, f.string ()); + + manifest_name_value nv (p.next ()); + if (!nv.name.empty () || nv.value != "1") + fail (l) << "unsupported manifest format in " << f; + + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + if (nv.name == "version") + { + try + { + v = standard_version (nv.value); + } + catch (const invalid_argument& e) + { + fail << "invalid standard version '" << nv.value << "': " << e; + } + break; + } + } + } + catch (const manifest_parsing& e) + { + location l (&f, e.line, e.column); + fail (l) << e.description; + } + catch (const io_error& e) + { + fail (l) << "unable to read from " << f << ": " << e; + } + catch (const system_error& e) // EACCES, etc. + { + fail (l) << "unable to access manifest " << f << ": " << e; + } + + if (v.empty ()) + fail (l) << "no version in " << f; + } + + // If this is the latest snapshot (i.e., the -a.1.z kind), then load the + // snapshot sn and id (e.g., commit date and id from git). If there is + // uncommitted stuff, then leave it as .z. + // + bool patched (false); + if (v.snapshot () && v.snapshot_sn == standard_version::latest_sn) + { + snapshot ss (extract_snapshot (rs)); + + if (!ss.empty ()) + { + v.snapshot_sn = ss.sn; + v.snapshot_id = move (ss.id); + patched = true; + } + } + + // Set all the version.* variables. + // + auto& vp (var_pool.rw (rs)); + + auto set = [&vp, &rs] (const char* var, auto val) + { + using T = decltype (val); + auto& v (vp.insert<T> (var, variable_visibility::project)); + rs.assign (v) = move (val); + }; + + // Enough of project version for unique identification (can be used in + // places like soname, etc). + // + string id (v.string_version ()); + if (v.snapshot ()) // Trailing dot already in id. + { + id += (v.snapshot_sn == standard_version::latest_sn + ? "z" + : (v.snapshot_id.empty () + ? to_string (v.snapshot_sn): + v.snapshot_id)); + } + + set ("version", v.string ()); // Package version. + + set ("version.project", v.string_project ()); + set ("version.project_number", v.version); + set ("version.project_id", move (id)); + + set ("version.epoch", uint64_t (v.epoch)); + + set ("version.major", uint64_t (v.major ())); + set ("version.minor", uint64_t (v.minor ())); + set ("version.patch", uint64_t (v.patch ())); + + set ("version.alpha", v.alpha ()); // bool + set ("version.beta", v.beta ()); // bool + set ("version.pre_release", v.alpha () || v.beta ()); + set ("version.pre_release_string", v.string_pre_release ()); + set ("version.pre_release_number", uint64_t (v.pre_release ())); + + set ("version.snapshot", v.snapshot ()); // bool + set ("version.snapshot_sn", v.snapshot_sn); // uint64 + set ("version.snapshot_id", v.snapshot_id); // string + set ("version.snapshot_string", v.string_snapshot ()); + + set ("version.revision", uint64_t (v.revision)); + + // Create the module. + // + mod.reset (new module (move (v), patched)); + } + + static void + dist_callback (const path&, const scope&, void*); + + bool + init (scope& rs, + scope&, + const location& l, + unique_ptr<module_base>& mod, + bool first, + bool, + const variable_map&) + { + tracer trace ("version::init"); + + if (!first) + fail (l) << "multiple version module initializations"; + + module& m (static_cast<module&> (*mod)); + const standard_version& v (m.version); + + // If the dist module has been loaded, set its dist.package and register + // the post-processing callback. + // + if (auto* dm = rs.modules.lookup<dist::module> (dist::module::name)) + { + // Don't touch if dist.package was set by the user. + // + value& val (rs.assign (dm->var_dist_package)); + + if (!val) + { + string p (cast<string> (rs.vars[var_project])); + p += '-'; + p += v.string (); + val = move (p); + + // Only register the post-processing callback if this a snapshot. + // + if (v.snapshot ()) + dm->register_callback (dir_path (".") / manifest, + &dist_callback, + &m); + } + } + + // Register rules. + // + { + auto& r (rs.rules); + + r.insert<doc> (perform_update_id, "version.doc", version_doc_); + r.insert<doc> (perform_clean_id, "version.doc", version_doc_); + r.insert<doc> (configure_update_id, "version.doc", version_doc_); + } + + return true; + } + + static void + dist_callback (const path& f, const scope& rs, void* data) + { + module& m (*static_cast<module*> (data)); + const standard_version v (m.version); + + // Complain if this is an uncommitted snapshot. + // + if (v.snapshot_sn == standard_version::latest_sn) + fail << "distribution of uncommitted project " << rs.src_path (); + + // The plan is simple, re-serialize the manifest into a temporary file + // fixing up the version. Then move the temporary file to the original. + // + path t; + try + { + permissions perm (path_permissions (f)); + + ifdstream ifs (f); + manifest_parser p (ifs, f.string ()); + + t = path::temp_path ("manifest"); + auto_fd ofd (fdopen (t, + fdopen_mode::out | + fdopen_mode::create | + fdopen_mode::exclusive | + fdopen_mode::binary, + perm)); + auto_rmfile arm (t); // Try to remove on failure ignoring errors. + + ofdstream ofs (move (ofd)); + manifest_serializer s (ofs, t.string ()); + + manifest_name_value nv (p.next ()); + assert (nv.name.empty () && nv.value == "1"); // We just loaded it. + s.next (nv.name, nv.value); + + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + if (nv.name == "version") + nv.value = v.string (); + + s.next (nv.name, nv.value); + } + + s.next (nv.name, nv.value); // End of manifest. + s.next (nv.name, nv.value); // End of stream. + + ofs.close (); + ifs.close (); + + mvfile (t, f, (cpflags::overwrite_content | + cpflags::overwrite_permissions)); + arm.cancel (); + } + catch (const manifest_parsing& e) + { + location l (&f, e.line, e.column); + fail (l) << e.description; + } + catch (const manifest_serialization& e) + { + location l (&t); + fail (l) << e.description; + } + catch (const io_error& e) + { + fail << "unable to overwrite " << f << ": " << e; + } + catch (const system_error& e) // EACCES, etc. + { + fail << "unable to overwrite " << f << ": " << e; + } + } + } +} diff --git a/build2/version/module b/build2/version/module new file mode 100644 index 0000000..d5c1f01 --- /dev/null +++ b/build2/version/module @@ -0,0 +1,32 @@ +// file : build2/version/module -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_VERSION_MODULE +#define BUILD2_VERSION_MODULE + +#include <build2/types> +#include <build2/utility> + +#include <butl/standard-version> + +#include <build2/module> + +namespace build2 +{ + namespace version + { + struct module: module_base + { + static const string name; + + butl::standard_version version; + bool version_patched; // True if snapshot was patched in. + + module (butl::standard_version v, bool vp) + : version (move (v)), version_patched (vp) {} + }; + } +} + +#endif // BUILD2_VERSION_MODULE diff --git a/build2/version/module.cxx b/build2/version/module.cxx new file mode 100644 index 0000000..9865271 --- /dev/null +++ b/build2/version/module.cxx @@ -0,0 +1,15 @@ +// file : build2/version/module.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <build2/version/module> + +using namespace std; + +namespace build2 +{ + namespace version + { + const string module::name ("version"); + } +} diff --git a/build2/version/rule b/build2/version/rule new file mode 100644 index 0000000..75a8c12 --- /dev/null +++ b/build2/version/rule @@ -0,0 +1,36 @@ +// file : build2/version/rule -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_VERSION_RULE +#define BUILD2_VERSION_RULE + +#include <build2/types> +#include <build2/utility> + +#include <build2/rule> + +namespace build2 +{ + namespace version + { + // Generate a version file. + // + class version_doc: public rule + { + public: + version_doc () {} + + virtual match_result + match (action, target&, const string&) const override; + + virtual recipe + apply (action, target&) const override; + + static target_state + perform_update (action, const target&); + }; + } +} + +#endif // BUILD2_VERSION_RULE diff --git a/build2/version/rule.cxx b/build2/version/rule.cxx new file mode 100644 index 0000000..1da37bb --- /dev/null +++ b/build2/version/rule.cxx @@ -0,0 +1,151 @@ +// file : build2/version/rule.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <build2/version/rule> + +#include <build2/scope> +#include <build2/target> +#include <build2/context> +#include <build2/algorithm> +#include <build2/filesystem> +#include <build2/diagnostics> + +#include <build2/version/module> + +using namespace std; +using namespace butl; + +namespace build2 +{ + namespace version + { + match_result version_doc:: + match (action a, target& xt, const string&) const + { + tracer trace ("version::version_file::match"); + + doc& t (static_cast<doc&> (xt)); + + // We match any doc{} target that is called version (potentially with + // some extension and different case) and that has a dependency on a + // file called manifest from the same project's src_root. + // + if (casecmp (t.name, "version") != 0) + { + l4 ([&]{trace << "name mismatch for target " << t;}); + return false; + } + + const scope& rs (t.root_scope ()); + + for (prerequisite_member p: group_prerequisite_members (a, t)) + { + if (!p.is_a<file> ()) + continue; + + const target& pt (p.search ()); + + if (pt.name != "manifest") + continue; + + const scope* prs (pt.base_scope ().root_scope ()); + + if (prs == nullptr || prs != &rs || pt.dir != rs.src_path ()) + continue; + + return true; + } + + l4 ([&]{trace << "no manifest prerequisite for target " << t;}); + return false; + } + + recipe version_doc:: + apply (action a, target& xt) const + { + doc& t (static_cast<doc&> (xt)); + + // Derive file names for the members. + // + t.derive_path (); + + // Inject dependency on the output directory. + // + inject_fsdir (a, t); + + // Match prerequisite members. + // + match_prerequisite_members (a, t); + + switch (a) + { + case perform_update_id: return &perform_update; + case perform_clean_id: return &perform_clean; // Standard clean. + default: return noop_recipe; // Configure update. + } + } + + target_state version_doc:: + perform_update (action a, const target& xt) + { + const doc& t (xt.as<const doc&> ()); + const path& f (t.path ()); + + const scope& rs (t.root_scope ()); + const module& m (*rs.modules.lookup<module> (module::name)); + + // Determine if anything needs to be updated. + // + // While we use the manifest file to decide whether we need to + // regenerate the version file, the version itself we get from the + // module (we checked above that manifest and version files are in the + // same project). + // + // That is, unless we patched the snapshot information in, in which case + // we have to compare the contents. + // + { + auto p (execute_prerequisites (a, t, t.load_mtime ())); + + if (!p.first) + { + if (!m.version_patched || !exists (f)) + return p.second; + + try + { + ifdstream ifs (f, fdopen_mode::in, ifdstream::badbit); + + string s; + getline (ifs, s); + + if (s == m.version.string_project ()) + return p.second; + } + catch (const io_error& e) + { + fail << "unable to read " << f << ": " << e; + } + } + } + + if (verb >= 2) + text << "cat >" << f; + + try + { + ofdstream ofs (f); + ofs << m.version.string_project () << endl; + ofs.close (); + } + catch (const io_error& e) + { + fail << "unable to write " << f << ": " << e; + } + + t.mtime (system_clock::now ()); + return target_state::changed; + } + } +} diff --git a/build2/version/snapshot b/build2/version/snapshot new file mode 100644 index 0000000..a279729 --- /dev/null +++ b/build2/version/snapshot @@ -0,0 +1,33 @@ +// file : build2/version/snapshot -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_VERSION_SNAPSHOT +#define BUILD2_VERSION_SNAPSHOT + +#include <build2/types> +#include <build2/utility> + +#include <build2/scope> + +namespace build2 +{ + namespace version + { + struct snapshot + { + uint64_t sn = 0; + string id; + + bool + empty () const {return sn == 0;} + }; + + // Return empty snapshot if unknown scm or uncommitted. + // + snapshot + extract_snapshot (const scope& rs); + } +} + +#endif // BUILD2_VERSION_SNAPSHOT diff --git a/build2/version/snapshot-git.cxx b/build2/version/snapshot-git.cxx new file mode 100644 index 0000000..b5db470 --- /dev/null +++ b/build2/version/snapshot-git.cxx @@ -0,0 +1,106 @@ +// file : build2/version/snapshot-git.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <build2/version/snapshot> + +using namespace std; + +namespace build2 +{ + namespace version + { + snapshot + extract_snapshot_git (const dir_path& src_root) + { + snapshot r; + const char* d (src_root.string ().c_str ()); + + // First check whether the working directory is clean. There doesn't + // seem to be a way to do everything in a single invocation (the + // porcelain v2 gives us the commit id but not timestamp). + // + + // If git status --porcelain returns anything, then the working + // directory is not clean. + // + { + const char* args[] {"git", "-C", d, "status", "--porcelain", nullptr}; + + if (!run<string> (args, [] (string& s) {return move (s);}).empty ()) + return r; + } + + // Now extract the commit id and date. + // + auto extract = [&r] (string& s) -> snapshot + { + if (s.compare (0, 5, "tree ") == 0) + { + // The 16-characters abbreviated commit id. + // + r.id.assign (s, 5, 16); + + if (r.id.size () != 16) + fail << "unable to extract git commit id from '" << s << "'"; + } + else if (s.compare (0, 10, "committer ") == 0) + try + { + // The line format is: + // + // committer <noise> <timestamp> <timezone> + // + // For example: + // + // committer John Doe <john@example.org> 1493117819 +0200 + // + // The timestamp is in seconds since UNIX epoch. The timezone + // appears to be always numeric (+0000 for UTC). + // + size_t p1 (s.rfind (' ')); // Can't be npos. + string tz (s, p1 + 1); + + size_t p2 (s.rfind (' ', p1 - 1)); + if (p2 == string::npos) + throw invalid_argument ("missing timestamp"); + + string ts (s, p2 + 1, p1 - p2 - 1); + r.sn = stoull (ts); + + if (tz.size () != 5) + throw invalid_argument ("invalid timezone"); + + unsigned long h (stoul (string (tz, 1, 2))); + unsigned long m (stoul (string (tz, 3, 2))); + unsigned long s (h * 3600 + m * 60); + + // The timezone indicates where the timestamp was generated so + // to convert to UTC we need to invert the sign. + // + switch (tz[0]) + { + case '+': r.sn -= s; break; + case '-': r.sn += s; break; + default: throw invalid_argument ("invalid timezone sign"); + } + } + catch (const invalid_argument& e) + { + fail << "unable to extract git commit date from '" << s << "': " << e; + } + + return (r.id.empty () || r.sn == 0) ? snapshot () : move (r); + }; + + const char* args[] { + "git", "-C", d, "cat-file", "commit", "HEAD", nullptr}; + r = run<snapshot> (args, extract); + + if (r.empty ()) + fail << "unable to extract git commit id/date for " << src_root; + + return r; + } + } +} diff --git a/build2/version/snapshot.cxx b/build2/version/snapshot.cxx new file mode 100644 index 0000000..be6c147 --- /dev/null +++ b/build2/version/snapshot.cxx @@ -0,0 +1,31 @@ +// file : build2/version/snapshot.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <build2/version/snapshot> + +#include <build2/filesystem> + +using namespace std; + +namespace build2 +{ + namespace version + { + snapshot + extract_snapshot_git (const dir_path&); + + static const dir_path git (".git"); + + snapshot + extract_snapshot (const scope& rs) + { + const dir_path& src_root (rs.src_path ()); + + if (exists (src_root / git)) + return extract_snapshot_git (src_root); + + return snapshot (); + } + } +} |