version.mjs (3451B)
1 /* 2 This file is part of GNU Taler 3 (C) 2026 Taler Systems S.A. 4 5 GNU Taler is free software; you can redistribute it and/or modify it under the 6 terms of the GNU General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 13 You should have received a copy of the GNU General Public License along with 14 GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 import { execFileSync } from "node:child_process"; 18 import { existsSync, readFileSync } from "node:fs"; 19 import { dirname, join, resolve } from "node:path"; 20 21 function repositoryRoot(startDirectory) { 22 let directory = resolve(startDirectory); 23 while (!existsSync(join(directory, "pnpm-workspace.yaml"))) { 24 const parent = dirname(directory); 25 if (parent === directory) { 26 throw Error( 27 `build version: cannot find the workspace from ${startDirectory}`, 28 ); 29 } 30 directory = parent; 31 } 32 return directory; 33 } 34 35 function validateBuildInfo(info) { 36 if ( 37 !info || 38 typeof info.tag !== "string" || 39 !/^v[0-9][0-9A-Za-z.+-]*$/.test(info.tag) || 40 typeof info.version !== "string" || 41 typeof info.gitHash !== "string" || 42 !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(info.gitHash) 43 ) { 44 throw Error("invalid version, tag, or commit hash"); 45 } 46 const version = info.tag.slice(1); 47 if (info.version !== version) { 48 const suffix = info.version.slice(version.length); 49 const match = /^-[1-9][0-9]*-g([0-9a-f]+)$/.exec(suffix); 50 if ( 51 !info.version.startsWith(version) || 52 !match || 53 !info.gitHash.startsWith(match[1]) 54 ) { 55 throw Error("version does not match the tag and commit hash"); 56 } 57 } 58 return { version: info.version, tag: info.tag, gitHash: info.gitHash }; 59 } 60 61 /** Resolve at invocation time, even when this helper is bundled into web-util. */ 62 export function getBuildInfo(startDirectory = process.cwd()) { 63 const root = repositoryRoot(startDirectory); 64 if (existsSync(join(root, ".git"))) { 65 const git = (args) => 66 execFileSync("git", args, { 67 cwd: root, 68 encoding: "utf8", 69 stdio: ["ignore", "pipe", "pipe"], 70 }).trim(); 71 try { 72 const description = git(["describe", "--tags", "--match", "v[0-9]*"]); 73 return validateBuildInfo({ 74 version: description.slice(1), 75 tag: description.replace(/-[0-9]+-g[0-9a-f]+$/, ""), 76 gitHash: git(["rev-parse", "HEAD"]), 77 }); 78 } catch (cause) { 79 throw Error( 80 "build version: cannot describe this checkout; ensure Git is installed and fetch the release tags and history (git fetch --tags; git fetch --unshallow for a shallow clone)", 81 { cause }, 82 ); 83 } 84 } 85 86 // An extracted archive can live inside another repository. Never use that 87 // enclosing repository's tags, or a package.json placeholder, as its version. 88 try { 89 return validateBuildInfo( 90 JSON.parse(readFileSync(join(root, "build-info.json"), "utf8")), 91 ); 92 } catch (cause) { 93 throw Error( 94 "build version: missing or invalid build-info.json; use a source archive created by make dist, or build from a checkout with release tags", 95 { cause }, 96 ); 97 } 98 }