Bläddra i källkod

execs.bump: a version-bump hook for USM manifests

A manifest's execs object can now declare a bump executable. When
usm manifest bump actually changes the version, the hook runs before
anything is rewritten, invoked as <bump> <old-version> <new-version>
from the source directory, so version strings living outside
MANIFEST.usm (source files, meson.build) stay in sync. A non-zero exit
or a spawn failure aborts the whole bump with exit code 251, leaving
the manifest and the tree untouched; flags-only bumps, same-version
bumps and bare --git releases never run the hook. A --git release
after a hook run stages the whole working tree (git add -A), since
what the hook edited cannot be known; without a hook the previous
MANIFEST.usm-only staging is preserved.

The lib's Executables mapper learns the key so validate and
materialisation see it; validate keeps bump permitted for data
packages (maintainer-side, like acquire) and the comment says why.
saddle-ClankerSaddle 16 timmar sedan
förälder
incheckning
83278aef1c
4 ändrade filer med 90 tillägg och 2 borttagningar
  1. 64 2
      src/cli/ManifestBump.vala
  2. 4 0
      src/lib/Exectuables.vala
  3. 4 0
      src/lib/Manifest.vala
  4. 18 0
      src/tests/TestMain.vala

+ 64 - 2
src/cli/ManifestBump.vala

@@ -20,6 +20,15 @@ private const string BUMP_REBUILD_DEPENDANTS_FLAG = "rebuildDependants";
  * pending so the tag captures the release state. `--push` pushes branch and
  * tag, implying `--git`.
  *
+ * When the manifest's `execs` object declares a `bump` executable and the
+ * version actually changes, the hook runs before anything is rewritten —
+ * invoked as `<bump> <old-version> <new-version>` from the current (source)
+ * directory — so it can update version strings elsewhere in the tree
+ * (source files, meson.build, …). A non-zero exit or a failure to spawn the
+ * hook aborts the whole bump, leaving the manifest and tree untouched. A
+ * `--git` release after a hook run stages the whole working tree, since what
+ * the hook edited cannot be known.
+ *
  * The manifest is parsed and rewritten as raw JSON rather than through
  * {@link Usm.Manifest}'s mapper, so a bump neither depends on manifest
  * validation nor rewrites flags the lib's flag enum has to know about.
@@ -122,7 +131,19 @@ public int manifest_bump(string[] args) {
     }
 
     bool changed = false;
+    bool hook_ran = false;
     if(new_version != null && new_version != current_version) {
+        // The bump hook runs after the new version is computed but before any
+        // file is touched, so a failing hook leaves the tree exactly as it was
+        string? bump_hook = bump_executable(root);
+        if(bump_hook != null) {
+            printerr(@"Running bump hook \"$bump_hook\" \"$current_version\" \"$new_version\"\n");
+            if(!bump_hook_run(bump_hook, current_version, new_version)) {
+                printerr(@"Error running bump exec: the bump hook \"$bump_hook\" failed, aborting the bump of \"$current_version\" → \"$new_version\"\n");
+                return 251;
+            }
+            hook_ran = true;
+        }
         try {
             root.set_native("version", new_version);
         }
@@ -208,8 +229,11 @@ public int manifest_bump(string[] args) {
     }
 
     // A version bump commits the manifest alone; a bare --git releases the
-    // current version, so every pending change belongs in the release commit
-    if(!bump_git_run(new string[] { "git", "add", spec != null ? "MANIFEST.usm" : "-A" })) {
+    // current version, so every pending change belongs in the release commit.
+    // A bump hook that ran may have edited anything in the tree, so its
+    // release commit stages the whole working tree too.
+    bool stage_everything = spec == null || hook_ran;
+    if(!bump_git_run(new string[] { "git", "add", stage_everything ? "-A" : "MANIFEST.usm" })) {
         printerr("Could not stage the release changes\n");
         return 250;
     }
@@ -249,6 +273,7 @@ public int manifest_bump(string[] args) {
 
 private int bump_usage() {
     printerr("USAGE:\n\tusm manifest bump [major|minor|patch|<version>] [--git] [--push] [--rebuild-dependants] [--no-rebuild-dependants]\n");
+    printerr("\n\tWhen the manifest's execs object declares a bump executable it runs as\n\t<bump> <old-version> <new-version> before MANIFEST.usm is rewritten; a\n\tnon-zero exit aborts the bump and leaves the tree untouched.\n");
     return 255;
 }
 
@@ -305,3 +330,40 @@ private bool bump_git_run(string[] command) {
         return false;
     }
 }
+
+
+/**
+ * Reads the manifest's `execs.bump` executable from the raw JSON tree, or
+ * null when the manifest declares none. The path is relative to the source
+ * directory, which is the current directory like acquire's.
+ */
+private string? bump_executable(InvercargillJson.JsonObject root) {
+    if(!root.has("execs")) {
+        return null;
+    }
+    var execs = root.get_object("execs");
+    if(execs == null || !execs.has("bump")) {
+        return null;
+    }
+    return execs.get_string("bump");
+}
+
+
+/**
+ * Runs the bump hook {@link hook} in the current (source) directory with the
+ * old and new version as arguments; its output flows to the terminal
+ * unchanged. Returns false — diagnostics already printed — when it fails to
+ * spawn or exits non-zero.
+ */
+private bool bump_hook_run(string hook, string old_version, string new_version) {
+    try {
+        return new Subprocess.newv(
+            { hook, old_version, new_version },
+            SubprocessFlags.INHERIT_FDS
+        ).wait_check();
+    }
+    catch(Error e) {
+        printerr(@"Could not run the bump hook \"$hook\": $(e.message)\n");
+        return false;
+    }
+}

+ 4 - 0
src/lib/Exectuables.vala

@@ -11,6 +11,7 @@ namespace Usm {
         public string? post_install { get; set; }
         public string? acquire { get; set; }
         public string? test { get; set; }
+        public string? bump { get; set; }
 
         public static PropertyMapper<Executables> get_mapper() {
             return PropertyMapper.build_for<Executables>(cfg => {
@@ -35,6 +36,9 @@ namespace Usm {
                 cfg.map<string?>("test", o => o.test, (o, v) => o.test = v)
                     .undefined_when(o => o.test == null)
                     .when_undefined(o => o.test = null);
+                cfg.map<string?>("bump", o => o.bump, (o, v) => o.bump = v)
+                    .undefined_when(o => o.bump == null)
+                    .when_undefined(o => o.bump = null);
                 cfg.set_constructor(() => new Executables());
             });
         }

+ 4 - 0
src/lib/Manifest.vala

@@ -145,6 +145,10 @@ namespace Usm {
             }
 
             var defined = new Vector<string>();
+            // bump, like acquire, is a maintainer-side executable rather than
+            // a lifecycle one (data packages have versions too, and their
+            // version strings live in files the bump hook rewrites), so it is
+            // deliberately absent from the forbidden list below
             if(executables.build != null) {
                 defined.add("build");
             }

+ 18 - 0
src/tests/TestMain.vala

@@ -142,6 +142,10 @@ namespace Usm.Tests {
         check(clean.executables.acquire != null, "acquire executable is honoured on data packages");
         check(clean.executables.build == null, "data package without build parses");
 
+        var bumping = manifest_from_json(DATA_MANIFEST.printf("\"execs\": { \"acquire\": \"fetch.sh\", \"bump\": \"usm-scripts/bump.sh\" }"));
+        bumping.validate();
+        check(bumping.executables.bump != null, "bump executable is permitted on data packages");
+
         var no_execs = manifest_from_json(DATA_MANIFEST.printf("\"execs\": { }"));
         no_execs.validate();
         check(no_execs.executables.build == null, "empty execs parse without a build executable");
@@ -175,6 +179,19 @@ namespace Usm.Tests {
         }
     }
 
+    void test_bump_executable() throws Error {
+        var manifest = manifest_from_json(DATA_MANIFEST.printf("\"execs\": { \"acquire\": \"fetch.sh\", \"bump\": \"usm-scripts/bump.sh\" }"));
+        check(manifest.executables.bump == "usm-scripts/bump.sh", "execs.bump parses through the manifest mapper");
+        check(manifest.executables.acquire == "fetch.sh", "execs.bump leaves neighbouring executables intact");
+
+        var serialised = ((!)new JsonElement.from_properties(Usm.Manifest.get_mapper().map_from(manifest))).stringify_pretty();
+        check(manifest_from_json(serialised).executables.bump == "usm-scripts/bump.sh",
+            "execs.bump round-trips through serialisation");
+
+        var without = manifest_from_json(DATA_MANIFEST.printf("\"execs\": { }"));
+        check(without.executables.bump == null, "absent execs.bump parses as null");
+    }
+
     void test_data_package_from_file() throws Error {
         var root = File.new_build_filename("/tmp", Uuid.string_random());
         root.make_directory();
@@ -3427,6 +3444,7 @@ echo "Installing libmeson-pkg.so to /usr/lib"
             test_data_package_flag();
             test_rebuild_dependants_flag();
             test_data_package_validation();
+            test_bump_executable();
             test_data_package_from_file();
             test_configuration_system_package_manager();
             test_gio_module_ref();