1
mirror of https://github.com/jakejarvis/hugo-extended.git synced 2026-06-12 08:45:27 -04:00

fix: support hugo's new macos .pkg installer (https://github.com/gohugoio/hugo/issues/14135)

- Replaced `careful-downloader` with custom download and checksum verification functions.
- Updated `log-symbols`, `read-pkg-up`, and added `adm-zip` and `tar` as dependencies.
- Enhanced installation process for macOS and other platforms, including cleanup of downloaded files.
- Adjusted binary path handling for macOS.
This commit is contained in:
2025-12-19 11:38:06 -05:00
parent 8004e3a98e
commit 6eb0302a83
6 changed files with 1206 additions and 1951 deletions
+130
View File
@@ -0,0 +1,130 @@
import globals from "globals";
export default [
{
ignores: [
"vendor/**",
"*.d.ts",
],
},
{
languageOptions: {
ecmaVersion: 2020,
sourceType: "module",
globals: {
...globals.node,
...globals.es6,
},
},
rules: {
"brace-style": "error",
camelcase: ["error", {
properties: "never",
ignoreDestructuring: true,
}],
"comma-dangle": ["error", {
arrays: "always-multiline",
objects: "always-multiline",
imports: "always-multiline",
exports: "always-multiline",
functions: "never",
}],
"comma-spacing": "error",
"comma-style": "error",
curly: ["error", "multi-line"],
"func-call-spacing": "error",
"no-multiple-empty-lines": ["error", {
max: 1,
}],
"no-tabs": "error",
"no-trailing-spaces": "error",
"object-curly-spacing": ["error", "always"],
"one-var": ["error", {
var: "never",
let: "never",
const: "never",
}],
"operator-linebreak": ["error", "after", { overrides: { "?": "before", ":": "before" } }],
"padded-blocks": ["error", "never"],
"quote-props": ["error", "as-needed"],
quotes: ["error", "double", {
avoidEscape: true,
allowTemplateLiterals: true,
}],
semi: "error",
"semi-spacing": "error",
"space-before-blocks": "error",
"space-before-function-paren": ["error", {
named: "never",
anonymous: "always",
asyncArrow: "always",
}],
"spaced-comment": ["error", "always", {
line: {
markers: ["/"],
exceptions: ["-", "+"],
},
block: {
markers: ["!"],
exceptions: ["*"],
balanced: true,
},
}],
"template-tag-spacing": ["error", "never"],
"arrow-parens": ["error", "always"],
"arrow-spacing": ["error", {
before: true,
after: true,
}],
"no-confusing-arrow": ["error", {
allowParens: true,
}],
"no-var": "error",
"prefer-const": ["error", {
destructuring: "any",
ignoreReadBeforeAssign: true,
}],
"prefer-destructuring": ["error", {
VariableDeclarator: {
array: false,
object: true,
},
AssignmentExpression: {
array: true,
object: false,
},
}],
"prefer-rest-params": "error",
"prefer-spread": "error",
"template-curly-spacing": "error",
},
},
{
files: ["test/**/*.js"],
languageOptions: {
globals: {
...globals.mocha,
},
},
},
];
+105 -21
View File
@@ -1,7 +1,11 @@
import path from "path";
import fs from "fs";
import { fileURLToPath } from "url";
import downloader from "careful-downloader";
import { spawnSync } from "child_process";
import { pipeline } from "stream/promises";
import crypto from "crypto";
import * as tar from "tar";
import AdmZip from "adm-zip";
import logSymbols from "log-symbols";
import {
getPkgVersion,
@@ -15,6 +19,41 @@ import {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
async function downloadFile(url, dest) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download ${url}: ${response.statusText}`);
}
await pipeline(response.body, fs.createWriteStream(dest));
}
async function verifyChecksum(filePath, checksumUrl, filename) {
const response = await fetch(checksumUrl);
if (!response.ok) {
throw new Error(`Failed to download checksums: ${response.statusText}`);
}
const checksums = await response.text();
// checksums file format: "sha256 filename"
const expectedChecksum = checksums
.split("\n")
.find((line) => line.endsWith(filename))
?.split(/\s+/)[0];
if (!expectedChecksum) {
throw new Error(`Checksum for ${filename} not found in checksums file.`);
}
const fileBuffer = fs.readFileSync(filePath);
const hash = crypto.createHash("sha256");
hash.update(fileBuffer);
const actualChecksum = hash.digest("hex");
if (actualChecksum !== expectedChecksum) {
throw new Error(`Checksum mismatch! Expected ${expectedChecksum}, got ${actualChecksum}`);
}
}
async function install() {
try {
const version = getPkgVersion();
@@ -22,36 +61,81 @@ async function install() {
const checksumFile = getChecksumFilename(version);
const binFile = getBinFilename();
// stop here if there's nothing we can download
if (!releaseFile)
if (!releaseFile) {
throw new Error(`Are you sure this platform is supported? See: https://github.com/gohugoio/hugo/releases/tag/v${version}`);
}
// warn if platform doesn't support Hugo Extended, proceed with vanilla Hugo
if (!isExtended(releaseFile))
if (!isExtended(releaseFile)) {
console.warn(`${logSymbols.info} Hugo Extended isn't supported on this platform, downloading vanilla Hugo instead.`);
}
// download release from GitHub and verify its checksum
const download = await downloader(getReleaseUrl(version, releaseFile), {
checksumUrl: getReleaseUrl(version, checksumFile),
filename: releaseFile,
destDir: path.join(__dirname, "..", "vendor"),
algorithm: "sha256",
extract: true,
});
// Prepare vendor directory
const vendorDir = path.join(__dirname, "..", "vendor");
if (!fs.existsSync(vendorDir)) {
fs.mkdirSync(vendorDir, { recursive: true });
}
// full path to the binary
const installedToPath = path.join(download, binFile);
const releaseUrl = getReleaseUrl(version, releaseFile);
const checksumUrl = getReleaseUrl(version, checksumFile);
const downloadPath = path.join(vendorDir, releaseFile);
// ensure hugo[.exe] is executable
fs.chmodSync(installedToPath, 0o755);
console.info(`${logSymbols.info} Downloading ${releaseFile}...`);
await downloadFile(releaseUrl, downloadPath);
console.info(`${logSymbols.info} Verifying checksum...`);
await verifyChecksum(downloadPath, checksumUrl, releaseFile);
if (process.platform === "darwin") {
console.info(`${logSymbols.info} Installing ${releaseFile} (requires sudo)...`);
// Run MacOS installer
const result = spawnSync("sudo", ["installer", "-pkg", downloadPath, "-target", "/"], {
stdio: "inherit",
});
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`Installer failed with exit code ${result.status}`);
}
// Cleanup downloaded pkg
fs.unlinkSync(downloadPath);
} else {
console.info(`${logSymbols.info} Extracting...`);
if (releaseFile.endsWith(".zip")) {
const zip = new AdmZip(downloadPath);
zip.extractAllTo(vendorDir, true);
// Cleanup zip
fs.unlinkSync(downloadPath);
} else if (releaseFile.endsWith(".tar.gz")) {
await tar.x({
file: downloadPath,
cwd: vendorDir,
});
// Cleanup tar.gz
fs.unlinkSync(downloadPath);
}
const binPath = path.join(vendorDir, binFile);
if (fs.existsSync(binPath)) {
fs.chmodSync(binPath, 0o755);
}
}
console.info(`${logSymbols.success} Hugo installed successfully!`);
console.info(getBinVersion(installedToPath));
// return the full path to our Hugo binary
return installedToPath;
// Check version
if (process.platform === "darwin") {
console.info(getBinVersion("/usr/local/bin/hugo"));
return "/usr/local/bin/hugo";
} else {
const binPath = path.join(vendorDir, binFile);
console.info(getBinVersion(binPath));
return binPath;
}
} catch (error) {
// pass whatever error occurred along the way to console
console.error(`${logSymbols.error} Hugo installation failed. :(`);
throw error;
}
+25 -21
View File
@@ -2,7 +2,7 @@ import path from "path";
import fs from "fs";
import { fileURLToPath } from "url";
import { execFileSync } from "child_process";
import { readPackageUpSync } from "read-pkg-up";
import { readPackageUpSync } from "read-package-up";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -26,11 +26,13 @@ export function getBinFilename() {
// Simple shortcut to ./vendor/hugo[.exe] from package root.
export function getBinPath() {
if (process.platform === "darwin") return "/usr/local/bin/hugo";
return path.join(
__dirname,
"..",
"vendor",
getBinFilename(),
getBinFilename()
);
}
@@ -44,12 +46,14 @@ export function getBinVersion(bin) {
// Simply detect if the given file exists.
export function doesBinExist(bin) {
try {
if (fs.existsSync(bin))
if (fs.existsSync(bin)) {
return true;
}
} catch (error) {
// something bad happened besides Hugo not existing
if (error.code !== "ENOENT")
if (error.code !== "ENOENT") {
throw error;
}
return false;
}
@@ -63,33 +67,33 @@ export function getReleaseFilename(version) {
const filename =
// macOS: as of 0.102.0, binaries are universal
platform === "darwin" && arch === "x64" ?
`hugo_extended_${version}_darwin-universal.tar.gz` :
platform === "darwin" && arch === "arm64" ?
`hugo_extended_${version}_darwin-universal.tar.gz` :
platform === "darwin" && arch === "x64"
? `hugo_extended_${version}_darwin-universal.pkg`
: platform === "darwin" && arch === "arm64"
? `hugo_extended_${version}_darwin-universal.pkg`
// Windows
platform === "win32" && arch === "x64" ?
`hugo_extended_${version}_windows-amd64.zip` :
platform === "win32" && arch === "arm64" ?
`hugo_${version}_windows-arm64.zip` :
: platform === "win32" && arch === "x64"
? `hugo_extended_${version}_windows-amd64.zip`
: platform === "win32" && arch === "arm64"
? `hugo_${version}_windows-arm64.zip`
// Linux
platform === "linux" && arch === "x64" ?
`hugo_extended_${version}_linux-amd64.tar.gz` :
platform === "linux" && arch === "arm64" ?
`hugo_extended_${version}_linux-arm64.tar.gz` :
: platform === "linux" && arch === "x64"
? `hugo_extended_${version}_linux-amd64.tar.gz`
: platform === "linux" && arch === "arm64"
? `hugo_extended_${version}_linux-arm64.tar.gz`
// FreeBSD
platform === "freebsd" && arch === "x64" ?
`hugo_${version}_freebsd-amd64.tar.gz` :
: platform === "freebsd" && arch === "x64"
? `hugo_${version}_freebsd-amd64.tar.gz`
// OpenBSD
platform === "openbsd" && arch === "x64" ?
`hugo_${version}_openbsd-amd64.tar.gz` :
: platform === "openbsd" && arch === "x64"
? `hugo_${version}_openbsd-amd64.tar.gz`
// not gonna work :(
null;
: null;
return filename;
}
+922 -1882
View File
File diff suppressed because it is too large Load Diff
+10 -23
View File
@@ -30,15 +30,17 @@
"exports": "./index.js",
"types": "./index.d.ts",
"dependencies": {
"careful-downloader": "^3.0.0",
"log-symbols": "^5.1.0",
"read-pkg-up": "^9.1.0"
"adm-zip": "^0.5.16",
"log-symbols": "^7.0.1",
"read-package-up": "^11.0.0",
"tar": "^7.5.2"
},
"devDependencies": {
"@jakejarvis/eslint-config": "*",
"del": "^7.1.0",
"eslint": "^8.56.0",
"mocha": "^10.3.0"
"@types/adm-zip": "^0.5.7",
"del": "^8.0.1",
"eslint": "^9.39.2",
"globals": "^16.5.0",
"mocha": "^11.7.5"
},
"scripts": {
"postinstall": "node postinstall.js",
@@ -65,20 +67,5 @@
"frontmatter",
"go",
"golang"
],
"eslintConfig": {
"extends": "@jakejarvis/eslint-config",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"env": {
"node": true,
"es6": true
},
"ignorePatterns": [
"vendor/**",
"*.d.ts"
]
}
]
}
+14 -4
View File
@@ -1,4 +1,4 @@
/* eslint-env node, mocha */
/* global it, assert -- Globals defined by Mocha */
import path from "path";
import { execFile } from "child_process";
import assert from "assert";
@@ -12,8 +12,9 @@ it("Hugo exists and runs?", async function () {
const hugoPath = await hugo();
assert(execFile(hugoPath, ["env"], function (error, stdout) {
if (error)
if (error) {
throw error;
}
console.log(stdout);
}));
@@ -22,14 +23,23 @@ it("Hugo exists and runs?", async function () {
it("Hugo doesn't exist, install it instead of throwing an error", async function () {
this.timeout(30000); // increase timeout to an excessive 30 seconds for CI
// On macOS, the binary is installed to /usr/local/bin, which we can't/shouldn't delete.
// We also can't easily test the reinstall flow without sudo.
if (process.platform === "darwin") {
console.log("Skipping reinstall test on macOS (requires sudo/system modification)");
this.skip();
return;
}
// delete binary to ensure it's auto-reinstalled
await deleteAsync(path.dirname(getBinPath()));
await deleteAsync(path.dirname(getBinPath()), { force: true });
const hugoPath = await hugo();
assert(execFile(hugoPath, ["version"], function (error, stdout) {
if (error)
if (error) {
throw error;
}
console.log(stdout);
}));