mirror of
https://github.com/dagu-org/dagu.git
synced 2025-12-28 06:34:22 +00:00
chore: fix npm package setup
This commit is contained in:
parent
5058ebf93f
commit
fae48f2f85
25
npm/dagu/bin/cli
Executable file
25
npm/dagu/bin/cli
Executable file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const path = require("path");
|
||||
const childProcess = require("child_process");
|
||||
|
||||
const { getPlatformPackage } = require("../lib/platform");
|
||||
|
||||
// Windows binaries end with .exe so we need to special case them.
|
||||
const binaryName = process.platform === "win32" ? "dagu.exe" : "dagu";
|
||||
|
||||
function getBinaryPath() {
|
||||
// Determine package name for this platform
|
||||
const platformSpecificPackageName = getPlatformPackage();
|
||||
|
||||
try {
|
||||
// Resolving will fail if the optionalDependency was not installed
|
||||
return require.resolve(`${platformSpecificPackageName}/bin/${binaryName}`);
|
||||
} catch (e) {
|
||||
return path.join(__dirname, "..", binaryName);
|
||||
}
|
||||
}
|
||||
|
||||
childProcess.execFileSync(getBinaryPath(), process.argv.slice(2), {
|
||||
stdio: "inherit",
|
||||
});
|
||||
@ -1,84 +1,20 @@
|
||||
/**
|
||||
* Dagu npm package - programmatic interface
|
||||
*/
|
||||
const path = require("path");
|
||||
const childProcess = require("child_process");
|
||||
|
||||
const { getBinaryPath } = require('./lib/platform');
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const { getPlatformPackage } = require("./lib/platform");
|
||||
|
||||
/**
|
||||
* Get the path to the Dagu binary
|
||||
* @returns {string|null} Path to the binary or null if not found
|
||||
*/
|
||||
function getDaguPath() {
|
||||
return getBinaryPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Dagu with given arguments
|
||||
* @param {string[]} args Command line arguments
|
||||
* @param {object} options Child process spawn options
|
||||
* @returns {ChildProcess} The spawned child process
|
||||
*/
|
||||
function execute(args = [], options = {}) {
|
||||
const binaryPath = getDaguPath();
|
||||
|
||||
if (!binaryPath) {
|
||||
throw new Error('Dagu binary not found. Please ensure Dagu is properly installed.');
|
||||
function getBinaryPath() {
|
||||
try {
|
||||
const platformSpecificPackageName = getPlatformPackage();
|
||||
// Resolving will fail if the optionalDependency was not installed
|
||||
return require.resolve(`${platformSpecificPackageName}/bin/${binaryName}`);
|
||||
} catch (e) {
|
||||
return path.join(__dirname, "..", binaryName);
|
||||
}
|
||||
|
||||
return spawn(binaryPath, args, {
|
||||
stdio: 'inherit',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Dagu and return a promise
|
||||
* @param {string[]} args Command line arguments
|
||||
* @param {object} options Child process spawn options
|
||||
* @returns {Promise<{code: number, signal: string|null}>} Exit code and signal
|
||||
*/
|
||||
function executeAsync(args = [], options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = execute(args, {
|
||||
stdio: 'pipe',
|
||||
...options
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
if (child.stdout) {
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
}
|
||||
|
||||
if (child.stderr) {
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
}
|
||||
|
||||
child.on('error', reject);
|
||||
|
||||
child.on('close', (code, signal) => {
|
||||
resolve({
|
||||
code,
|
||||
signal,
|
||||
stdout,
|
||||
stderr
|
||||
});
|
||||
});
|
||||
module.exports.runBinary = function (...args) {
|
||||
childProcess.execFileSync(getBinaryPath(), args, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getDaguPath,
|
||||
execute,
|
||||
executeAsync,
|
||||
// Re-export useful functions
|
||||
getBinaryPath,
|
||||
getPlatformInfo: require('./lib/platform').getPlatformInfo
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,135 +1,109 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getBinaryPath, getPlatformPackage, setPlatformBinary, getPlatformInfo } = require('./lib/platform');
|
||||
const { downloadBinary, downloadBinaryFromNpm } = require('./lib/download');
|
||||
const { validateBinary } = require('./lib/validate');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const zlib = require("zlib");
|
||||
const https = require("https");
|
||||
|
||||
async function install() {
|
||||
console.log('Installing Dagu...');
|
||||
|
||||
try {
|
||||
// Check if running in CI or with --ignore-scripts
|
||||
if (process.env.npm_config_ignore_scripts === 'true') {
|
||||
console.log('Skipping postinstall script (--ignore-scripts flag detected)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to resolve platform-specific package
|
||||
const platformPackage = getPlatformPackage();
|
||||
if (!platformPackage) {
|
||||
console.error(`
|
||||
Error: Unsupported platform: ${process.platform}-${process.arch}
|
||||
const { getPlatformPackage } = require("./lib/platform");
|
||||
|
||||
Dagu does not provide pre-built binaries for this platform.
|
||||
Please build from source: https://github.com/dagu-org/dagu#building-from-source
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Detected platform: ${process.platform}-${process.arch}`);
|
||||
console.log(`Looking for package: ${platformPackage}`);
|
||||
|
||||
// Check for cross-platform scenario
|
||||
const { checkCrossPlatformScenario } = require('./lib/platform');
|
||||
const crossPlatformWarning = checkCrossPlatformScenario();
|
||||
if (crossPlatformWarning) {
|
||||
console.warn(`\n${crossPlatformWarning.message}\n`);
|
||||
}
|
||||
|
||||
// Check if binary already exists from optionalDependency
|
||||
const existingBinary = getBinaryPath();
|
||||
if (existingBinary && fs.existsSync(existingBinary)) {
|
||||
console.log('Using pre-installed binary from optional dependency');
|
||||
|
||||
// Validate the binary
|
||||
if (await validateBinary(existingBinary)) {
|
||||
console.log('✓ Dagu installation complete!');
|
||||
return;
|
||||
} else {
|
||||
console.warn('Binary validation failed, attempting to download...');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Optional dependency not found, downloading binary...');
|
||||
}
|
||||
|
||||
// Fallback: Download binary
|
||||
try {
|
||||
const binaryPath = path.join(__dirname, 'bin', process.platform === 'win32' ? 'dagu.exe' : 'dagu');
|
||||
|
||||
// Create bin directory if it doesn't exist
|
||||
const binDir = path.dirname(binaryPath);
|
||||
if (!fs.existsSync(binDir)) {
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Skip download in development if flag file exists
|
||||
if (fs.existsSync(path.join(__dirname, '.skip-install'))) {
|
||||
console.log('Development mode: skipping binary download (.skip-install file found)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Download the binary
|
||||
await downloadBinary(binaryPath, { method: 'auto' });
|
||||
|
||||
// Validate the downloaded binary
|
||||
if (await validateBinary(binaryPath)) {
|
||||
setPlatformBinary(binaryPath);
|
||||
console.log('✓ Dagu installation complete!');
|
||||
|
||||
// Print warning about optionalDependencies if none were found
|
||||
if (!hasAnyOptionalDependency()) {
|
||||
console.warn(`
|
||||
⚠ WARNING: optionalDependencies may be disabled in your environment.
|
||||
For better performance and reliability, consider enabling them.
|
||||
See: https://docs.npmjs.com/cli/v8/using-npm/config#optional
|
||||
`);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Downloaded binary validation failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to install Dagu:', error.message);
|
||||
console.error(`
|
||||
Platform details:
|
||||
${JSON.stringify(getPlatformInfo(), null, 2)}
|
||||
// Windows binaries end with .exe so we need to special case them.
|
||||
const binaryName = process.platform === "win32" ? "dagu.exe" : "dagu";
|
||||
|
||||
Please try one of the following:
|
||||
1. Install manually from: https://github.com/dagu-org/dagu/releases
|
||||
2. Build from source: https://github.com/dagu-org/dagu#building-from-source
|
||||
3. Report this issue: https://github.com/dagu-org/dagu/issues
|
||||
`);
|
||||
process.exit(1);
|
||||
// Adjust the version you want to install. You can also make this dynamic.
|
||||
const PACKAGE_VERSION = require("./package.json").version;
|
||||
|
||||
// Compute the path we want to emit the fallback binary to
|
||||
const fallbackBinaryPath = path.join(__dirname, binaryName);
|
||||
|
||||
function makeRequest(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
https
|
||||
.get(url, (response) => {
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
const chunks = [];
|
||||
response.on("data", (chunk) => chunks.push(chunk));
|
||||
response.on("end", () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
} else if (
|
||||
response.statusCode >= 300 &&
|
||||
response.statusCode < 400 &&
|
||||
response.headers.location
|
||||
) {
|
||||
// Follow redirects
|
||||
makeRequest(response.headers.location).then(resolve, reject);
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`npm responded with status code ${response.statusCode} when downloading the package!`
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function extractFileFromTarball(tarballBuffer, filepath) {
|
||||
// Tar archives are organized in 512 byte blocks.
|
||||
// Blocks can either be header blocks or data blocks.
|
||||
// Header blocks contain file names of the archive in the first 100 bytes, terminated by a null byte.
|
||||
// The size of a file is contained in bytes 124-135 of a header block and in octal format.
|
||||
// The following blocks will be data blocks containing the file.
|
||||
let offset = 0;
|
||||
while (offset < tarballBuffer.length) {
|
||||
const header = tarballBuffer.subarray(offset, offset + 512);
|
||||
offset += 512;
|
||||
|
||||
const fileName = header.toString("utf-8", 0, 100).replace(/\0.*/g, "");
|
||||
const fileSize = parseInt(
|
||||
header.toString("utf-8", 124, 136).replace(/\0.*/g, ""),
|
||||
8
|
||||
);
|
||||
|
||||
if (fileName === filepath) {
|
||||
return tarballBuffer.subarray(offset, offset + fileSize);
|
||||
}
|
||||
|
||||
// Clamp offset to the uppoer multiple of 512
|
||||
offset = (offset + fileSize + 511) & ~511;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any optional dependency is installed
|
||||
function hasAnyOptionalDependency() {
|
||||
const pkg = require('./package.json');
|
||||
const optionalDeps = Object.keys(pkg.optionalDependencies || {});
|
||||
|
||||
for (const dep of optionalDeps) {
|
||||
try {
|
||||
require.resolve(dep);
|
||||
return true;
|
||||
} catch (e) {
|
||||
// Continue checking
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
async function downloadBinaryFromNpm() {
|
||||
console.log({
|
||||
getPlatformPackage,
|
||||
});
|
||||
// Determine package name for this platform
|
||||
const platformSpecificPackageName = getPlatformPackage();
|
||||
|
||||
const url = `https://registry.npmjs.org/@dagu-org/${platformSpecificPackageName}/-/${platformSpecificPackageName}-${PACKAGE_VERSION}.tgz`;
|
||||
console.log(`Downloading binary distribution package from ${url}...`);
|
||||
// Download the tarball of the right binary distribution package
|
||||
const tarballDownloadBuffer = await makeRequest(url);
|
||||
const tarballBuffer = zlib.unzipSync(tarballDownloadBuffer);
|
||||
|
||||
console.log(fallbackBinaryPath);
|
||||
|
||||
// Extract binary from package and write to disk
|
||||
fs.writeFileSync(
|
||||
fallbackBinaryPath,
|
||||
extractFileFromTarball(tarballBuffer, `package/bin/${binaryName}`),
|
||||
{ mode: 0o755 } // Make binary file executable
|
||||
);
|
||||
}
|
||||
|
||||
// Handle errors gracefully
|
||||
process.on('unhandledRejection', (error) => {
|
||||
console.error('Installation error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Run installation
|
||||
install().catch((error) => {
|
||||
console.error('Installation failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
// Skip downloading the binary if it was already installed via optionalDependencies
|
||||
if (!isPlatformSpecificPackageInstalled()) {
|
||||
console.log(
|
||||
"Platform specific package not found. Will manually download binary."
|
||||
);
|
||||
downloadBinaryFromNpm();
|
||||
} else {
|
||||
console.log(
|
||||
"Platform specific package already installed. Will fall back to manually downloading binary."
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,230 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const os = require('os');
|
||||
|
||||
/**
|
||||
* Get cache directory for binaries
|
||||
* @returns {string} Cache directory path
|
||||
*/
|
||||
function getCacheDir() {
|
||||
// Use standard cache locations based on platform
|
||||
const homeDir = os.homedir();
|
||||
let cacheDir;
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Windows: %LOCALAPPDATA%\dagu-cache
|
||||
cacheDir = path.join(process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'), 'dagu-cache');
|
||||
} else if (process.platform === 'darwin') {
|
||||
// macOS: ~/Library/Caches/dagu
|
||||
cacheDir = path.join(homeDir, 'Library', 'Caches', 'dagu');
|
||||
} else {
|
||||
// Linux/BSD: ~/.cache/dagu
|
||||
cacheDir = path.join(process.env.XDG_CACHE_HOME || path.join(homeDir, '.cache'), 'dagu');
|
||||
}
|
||||
|
||||
// Allow override via environment variable
|
||||
if (process.env.DAGU_CACHE_DIR) {
|
||||
cacheDir = process.env.DAGU_CACHE_DIR;
|
||||
}
|
||||
|
||||
return cacheDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached binary path
|
||||
* @param {string} version Version of the binary
|
||||
* @param {string} platform Platform identifier
|
||||
* @returns {string} Path to cached binary
|
||||
*/
|
||||
function getCachedBinaryPath(version, platform) {
|
||||
const cacheDir = getCacheDir();
|
||||
const binaryName = process.platform === 'win32' ? 'dagu.exe' : 'dagu';
|
||||
return path.join(cacheDir, `${version}-${platform}`, binaryName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if binary exists in cache
|
||||
* @param {string} version Version of the binary
|
||||
* @param {string} platform Platform identifier
|
||||
* @returns {boolean} True if cached, false otherwise
|
||||
*/
|
||||
function isCached(version, platform) {
|
||||
const cachedPath = getCachedBinaryPath(version, platform);
|
||||
return fs.existsSync(cachedPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save binary to cache
|
||||
* @param {string} sourcePath Path to the binary to cache
|
||||
* @param {string} version Version of the binary
|
||||
* @param {string} platform Platform identifier
|
||||
* @returns {string} Path to cached binary
|
||||
*/
|
||||
function cacheBinary(sourcePath, version, platform) {
|
||||
const cachedPath = getCachedBinaryPath(version, platform);
|
||||
const cacheDir = path.dirname(cachedPath);
|
||||
|
||||
// Create cache directory if it doesn't exist
|
||||
if (!fs.existsSync(cacheDir)) {
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Copy binary to cache
|
||||
fs.copyFileSync(sourcePath, cachedPath);
|
||||
|
||||
// Preserve executable permissions
|
||||
if (process.platform !== 'win32') {
|
||||
fs.chmodSync(cachedPath, 0o755);
|
||||
}
|
||||
|
||||
// Create a metadata file with cache info
|
||||
const metadataPath = path.join(cacheDir, 'metadata.json');
|
||||
const metadata = {
|
||||
version,
|
||||
platform,
|
||||
cachedAt: new Date().toISOString(),
|
||||
checksum: calculateChecksum(cachedPath)
|
||||
};
|
||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
||||
|
||||
return cachedPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get binary from cache
|
||||
* @param {string} version Version of the binary
|
||||
* @param {string} platform Platform identifier
|
||||
* @returns {string|null} Path to cached binary or null if not found
|
||||
*/
|
||||
function getCachedBinary(version, platform) {
|
||||
if (!isCached(version, platform)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cachedPath = getCachedBinaryPath(version, platform);
|
||||
|
||||
// Verify the cached binary still works
|
||||
try {
|
||||
fs.accessSync(cachedPath, fs.constants.X_OK);
|
||||
return cachedPath;
|
||||
} catch (e) {
|
||||
// Cached binary is corrupted or not executable
|
||||
// Remove it from cache
|
||||
cleanCacheEntry(version, platform);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate checksum of a file
|
||||
* @param {string} filePath Path to the file
|
||||
* @returns {string} SHA256 checksum
|
||||
*/
|
||||
function calculateChecksum(filePath) {
|
||||
const hash = crypto.createHash('sha256');
|
||||
const data = fs.readFileSync(filePath);
|
||||
hash.update(data);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean specific cache entry
|
||||
* @param {string} version Version of the binary
|
||||
* @param {string} platform Platform identifier
|
||||
*/
|
||||
function cleanCacheEntry(version, platform) {
|
||||
const cacheDir = path.dirname(getCachedBinaryPath(version, platform));
|
||||
|
||||
if (fs.existsSync(cacheDir)) {
|
||||
fs.rmSync(cacheDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean old cache entries (older than specified days)
|
||||
* @param {number} maxAgeDays Maximum age in days (default 30)
|
||||
*/
|
||||
function cleanOldCache(maxAgeDays = 30) {
|
||||
const cacheDir = getCacheDir();
|
||||
|
||||
if (!fs.existsSync(cacheDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(cacheDir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(cacheDir, entry);
|
||||
const metadataPath = path.join(entryPath, 'metadata.json');
|
||||
|
||||
if (fs.existsSync(metadataPath)) {
|
||||
try {
|
||||
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
||||
const cachedAt = new Date(metadata.cachedAt).getTime();
|
||||
|
||||
if (now - cachedAt > maxAgeMs) {
|
||||
fs.rmSync(entryPath, { recursive: true, force: true });
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid metadata, remove entry
|
||||
fs.rmSync(entryPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache size
|
||||
* @returns {number} Total size in bytes
|
||||
*/
|
||||
function getCacheSize() {
|
||||
const cacheDir = getCacheDir();
|
||||
|
||||
if (!fs.existsSync(cacheDir)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let totalSize = 0;
|
||||
|
||||
function calculateDirSize(dirPath) {
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
calculateDirSize(fullPath);
|
||||
} else {
|
||||
const stats = fs.statSync(fullPath);
|
||||
totalSize += stats.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
calculateDirSize(cacheDir);
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCacheDir,
|
||||
getCachedBinaryPath,
|
||||
isCached,
|
||||
cacheBinary,
|
||||
getCachedBinary,
|
||||
cleanCacheEntry,
|
||||
cleanOldCache,
|
||||
getCacheSize
|
||||
};
|
||||
@ -1,60 +0,0 @@
|
||||
/**
|
||||
* Constants for Dagu npm distribution
|
||||
*/
|
||||
|
||||
const GITHUB_ORG = 'dagu-org';
|
||||
const GITHUB_REPO = 'dagu';
|
||||
const NPM_ORG = '@dagu-org';
|
||||
|
||||
// Tier classification for platform support
|
||||
const PLATFORM_TIERS = {
|
||||
TIER_1: [
|
||||
'linux-x64',
|
||||
'linux-arm64',
|
||||
'darwin-x64',
|
||||
'darwin-arm64',
|
||||
'win32-x64'
|
||||
],
|
||||
TIER_2: [
|
||||
'linux-ia32',
|
||||
'linux-armv7',
|
||||
'win32-ia32',
|
||||
'freebsd-x64'
|
||||
],
|
||||
TIER_3: [
|
||||
'linux-armv6',
|
||||
'linux-ppc64',
|
||||
'linux-s390x',
|
||||
'win32-arm64',
|
||||
'freebsd-arm64',
|
||||
'freebsd-ia32',
|
||||
'freebsd-arm',
|
||||
'openbsd-x64',
|
||||
'openbsd-arm64'
|
||||
]
|
||||
};
|
||||
|
||||
// Error messages
|
||||
const ERRORS = {
|
||||
UNSUPPORTED_PLATFORM: 'Unsupported platform',
|
||||
DOWNLOAD_FAILED: 'Failed to download binary',
|
||||
VALIDATION_FAILED: 'Binary validation failed',
|
||||
CHECKSUM_MISMATCH: 'Checksum verification failed',
|
||||
EXTRACTION_FAILED: 'Failed to extract archive'
|
||||
};
|
||||
|
||||
// URLs
|
||||
const URLS = {
|
||||
RELEASES: `https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/releases`,
|
||||
ISSUES: `https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/issues`,
|
||||
BUILD_DOCS: `https://github.com/${GITHUB_ORG}/${GITHUB_REPO}#building-from-source`
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
GITHUB_ORG,
|
||||
GITHUB_REPO,
|
||||
NPM_ORG,
|
||||
PLATFORM_TIERS,
|
||||
ERRORS,
|
||||
URLS
|
||||
};
|
||||
@ -1,404 +0,0 @@
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const zlib = require('zlib');
|
||||
const tar = require('tar');
|
||||
const { getCachedBinary, cacheBinary, cleanOldCache } = require('./cache');
|
||||
|
||||
// Get package version
|
||||
const PACKAGE_VERSION = require('../package.json').version;
|
||||
const GITHUB_RELEASES_URL = 'https://github.com/dagu-org/dagu/releases/download';
|
||||
|
||||
/**
|
||||
* Map Node.js platform/arch to goreleaser asset names
|
||||
*/
|
||||
function getAssetName(version) {
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
|
||||
// Platform name mapping (matches goreleaser output - lowercase)
|
||||
const osMap = {
|
||||
'darwin': 'darwin',
|
||||
'linux': 'linux',
|
||||
'win32': 'windows',
|
||||
'freebsd': 'freebsd',
|
||||
'openbsd': 'openbsd'
|
||||
};
|
||||
|
||||
// Architecture name mapping (matches goreleaser output)
|
||||
const archMap = {
|
||||
'x64': 'amd64',
|
||||
'ia32': '386',
|
||||
'arm64': 'arm64',
|
||||
'ppc64': 'ppc64le',
|
||||
's390x': 's390x'
|
||||
};
|
||||
|
||||
let osName = osMap[platform] || platform;
|
||||
let archName = archMap[arch] || arch;
|
||||
|
||||
// Special handling for ARM
|
||||
if (arch === 'arm' && platform === 'linux') {
|
||||
const { getArmVariant } = require('./platform');
|
||||
const variant = getArmVariant();
|
||||
archName = `armv${variant}`;
|
||||
}
|
||||
|
||||
// All assets are .tar.gz now (goreleaser changed this)
|
||||
const ext = '.tar.gz';
|
||||
return `dagu_${version}_${osName}_${archName}${ext}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make HTTP request and return buffer (Sentry-style)
|
||||
*/
|
||||
function makeRequest(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
https
|
||||
.get(url, (response) => {
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
const chunks = [];
|
||||
response.on('data', (chunk) => chunks.push(chunk));
|
||||
response.on('end', () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
} else if (
|
||||
response.statusCode >= 300 &&
|
||||
response.statusCode < 400 &&
|
||||
response.headers.location
|
||||
) {
|
||||
// Follow redirects
|
||||
makeRequest(response.headers.location).then(resolve, reject);
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`Server responded with status code ${response.statusCode} when downloading the package!`
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file with progress reporting
|
||||
*/
|
||||
async function downloadFile(url, destination, options = {}) {
|
||||
const { onProgress, maxRetries = 3 } = options;
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await downloadFileAttempt(url, destination, { onProgress, attempt });
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`Download failed (attempt ${attempt}/${maxRetries}), retrying...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Exponential backoff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single download attempt
|
||||
*/
|
||||
function downloadFileAttempt(url, destination, options = {}) {
|
||||
const { onProgress, attempt = 1 } = options;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const tempFile = `${destination}.download.${process.pid}.tmp`;
|
||||
|
||||
https.get(url, (response) => {
|
||||
// Handle redirects
|
||||
if (response.statusCode === 301 || response.statusCode === 302) {
|
||||
const redirectUrl = response.headers.location;
|
||||
if (!redirectUrl) {
|
||||
reject(new Error('Redirect location not provided'));
|
||||
return;
|
||||
}
|
||||
downloadFileAttempt(redirectUrl, destination, options)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const totalSize = parseInt(response.headers['content-length'], 10);
|
||||
let downloadedSize = 0;
|
||||
|
||||
const fileStream = fs.createWriteStream(tempFile);
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
downloadedSize += chunk.length;
|
||||
if (onProgress && totalSize) {
|
||||
const percentage = Math.round((downloadedSize / totalSize) * 100);
|
||||
onProgress(percentage, downloadedSize, totalSize);
|
||||
}
|
||||
});
|
||||
|
||||
response.pipe(fileStream);
|
||||
|
||||
fileStream.on('finish', () => {
|
||||
fileStream.close(() => {
|
||||
// Move temp file to final destination
|
||||
fs.renameSync(tempFile, destination);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
fileStream.on('error', (err) => {
|
||||
fs.unlinkSync(tempFile);
|
||||
reject(err);
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
if (fs.existsSync(tempFile)) {
|
||||
fs.unlinkSync(tempFile);
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract archive based on file extension
|
||||
*/
|
||||
async function extractArchive(archivePath, outputDir) {
|
||||
const ext = path.extname(archivePath).toLowerCase();
|
||||
|
||||
if (ext === '.gz' || archivePath.endsWith('.tar.gz')) {
|
||||
// Extract tar.gz
|
||||
await tar.extract({
|
||||
file: archivePath,
|
||||
cwd: outputDir,
|
||||
filter: (path) => path === 'dagu' || path === 'dagu.exe'
|
||||
});
|
||||
} else if (ext === '.zip') {
|
||||
// For Windows, we need a zip extractor
|
||||
// Using built-in Windows extraction via PowerShell
|
||||
const { execSync } = require('child_process');
|
||||
const command = `powershell -command "Expand-Archive -Path '${archivePath}' -DestinationPath '${outputDir}' -Force"`;
|
||||
execSync(command);
|
||||
} else {
|
||||
throw new Error(`Unsupported archive format: ${ext}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and verify checksums
|
||||
*/
|
||||
async function downloadChecksums(version) {
|
||||
const checksumsUrl = `${GITHUB_RELEASES_URL}/v${version}/checksums.txt`;
|
||||
const tempFile = path.join(require('os').tmpdir(), `dagu-checksums-${process.pid}.txt`);
|
||||
|
||||
try {
|
||||
await downloadFile(checksumsUrl, tempFile);
|
||||
const content = fs.readFileSync(tempFile, 'utf8');
|
||||
|
||||
// Parse checksums file
|
||||
const checksums = {};
|
||||
content.split('\n').forEach(line => {
|
||||
const match = line.match(/^([a-f0-9]{64})\s+(.+)$/);
|
||||
if (match) {
|
||||
checksums[match[2]] = match[1];
|
||||
}
|
||||
});
|
||||
|
||||
return checksums;
|
||||
} finally {
|
||||
if (fs.existsSync(tempFile)) {
|
||||
fs.unlinkSync(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify file checksum
|
||||
*/
|
||||
function verifyChecksum(filePath, expectedChecksum) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash('sha256');
|
||||
const stream = fs.createReadStream(filePath);
|
||||
|
||||
stream.on('data', (data) => hash.update(data));
|
||||
stream.on('end', () => {
|
||||
const actualChecksum = hash.digest('hex');
|
||||
if (actualChecksum === expectedChecksum) {
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error(`Checksum mismatch: expected ${expectedChecksum}, got ${actualChecksum}`));
|
||||
}
|
||||
});
|
||||
stream.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract file from npm tarball (aligned with Sentry approach)
|
||||
*/
|
||||
function extractFileFromTarball(tarballBuffer, filepath) {
|
||||
// Tar archives are organized in 512 byte blocks.
|
||||
// Blocks can either be header blocks or data blocks.
|
||||
// Header blocks contain file names of the archive in the first 100 bytes, terminated by a null byte.
|
||||
// The size of a file is contained in bytes 124-135 of a header block and in octal format.
|
||||
// The following blocks will be data blocks containing the file.
|
||||
let offset = 0;
|
||||
while (offset < tarballBuffer.length) {
|
||||
const header = tarballBuffer.subarray(offset, offset + 512);
|
||||
offset += 512;
|
||||
|
||||
const fileName = header.toString('utf-8', 0, 100).replace(/\0.*/g, '');
|
||||
const fileSize = parseInt(header.toString('utf-8', 124, 136).replace(/\0.*/g, ''), 8);
|
||||
|
||||
if (fileName === filepath) {
|
||||
return tarballBuffer.subarray(offset, offset + fileSize);
|
||||
}
|
||||
|
||||
// Clamp offset to the upper multiple of 512
|
||||
offset = (offset + fileSize + 511) & ~511;
|
||||
}
|
||||
|
||||
throw new Error(`File ${filepath} not found in tarball`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download binary from npm registry (Sentry-style)
|
||||
*/
|
||||
async function downloadBinaryFromNpm(version) {
|
||||
const { getPlatformPackage } = require('./platform');
|
||||
const platformPackage = getPlatformPackage();
|
||||
|
||||
if (!platformPackage) {
|
||||
throw new Error('Platform not supported!');
|
||||
}
|
||||
|
||||
const packageName = platformPackage.replace('@dagu-org/', '');
|
||||
const binaryName = process.platform === 'win32' ? 'dagu.exe' : 'dagu';
|
||||
|
||||
console.log(`Downloading ${platformPackage} from npm registry...`);
|
||||
|
||||
// Download the tarball of the right binary distribution package
|
||||
const tarballUrl = `https://registry.npmjs.org/${platformPackage}/-/${packageName}-${version}.tgz`;
|
||||
const tarballDownloadBuffer = await makeRequest(tarballUrl);
|
||||
const tarballBuffer = zlib.unzipSync(tarballDownloadBuffer);
|
||||
|
||||
// Extract binary from package
|
||||
const binaryData = extractFileFromTarball(tarballBuffer, `package/bin/${binaryName}`);
|
||||
|
||||
return binaryData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main download function
|
||||
*/
|
||||
async function downloadBinary(destination, options = {}) {
|
||||
const version = options.version || PACKAGE_VERSION;
|
||||
const { method = 'auto', useCache = true } = options;
|
||||
const platformKey = `${process.platform}-${process.arch}`;
|
||||
|
||||
console.log(`Installing Dagu v${version} for ${platformKey}...`);
|
||||
|
||||
// Check cache first
|
||||
if (useCache) {
|
||||
const cachedBinary = getCachedBinary(version, platformKey);
|
||||
if (cachedBinary) {
|
||||
console.log('✓ Using cached binary');
|
||||
fs.copyFileSync(cachedBinary, destination);
|
||||
if (process.platform !== 'win32') {
|
||||
fs.chmodSync(destination, 0o755);
|
||||
}
|
||||
// Clean old cache entries
|
||||
cleanOldCache();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let binaryData;
|
||||
|
||||
if (method === 'npm' || method === 'auto') {
|
||||
// Try npm registry first (following Sentry's approach)
|
||||
try {
|
||||
binaryData = await downloadBinaryFromNpm(version);
|
||||
console.log('✓ Downloaded from npm registry');
|
||||
} catch (npmError) {
|
||||
if (method === 'npm') {
|
||||
throw npmError;
|
||||
}
|
||||
console.log('npm registry download failed, trying GitHub releases...');
|
||||
}
|
||||
}
|
||||
|
||||
if (!binaryData && (method === 'github' || method === 'auto')) {
|
||||
// Fallback to GitHub releases
|
||||
const assetName = getAssetName(version);
|
||||
const downloadUrl = `${GITHUB_RELEASES_URL}/v${version}/${assetName}`;
|
||||
|
||||
const tempFile = path.join(require('os').tmpdir(), `dagu-${process.pid}-${Date.now()}.tmp`);
|
||||
|
||||
try {
|
||||
await downloadFile(downloadUrl, tempFile, {
|
||||
onProgress: (percentage, downloaded, total) => {
|
||||
const mb = (size) => (size / 1024 / 1024).toFixed(2);
|
||||
process.stdout.write(`\rProgress: ${percentage}% (${mb(downloaded)}MB / ${mb(total)}MB)`);
|
||||
}
|
||||
});
|
||||
console.log('\n✓ Downloaded from GitHub releases');
|
||||
|
||||
// Extract from archive
|
||||
const binaryName = process.platform === 'win32' ? 'dagu.exe' : 'dagu';
|
||||
|
||||
// All files are .tar.gz now
|
||||
const archiveData = fs.readFileSync(tempFile);
|
||||
const tarData = zlib.gunzipSync(archiveData);
|
||||
binaryData = extractFileFromTarball(tarData, binaryName);
|
||||
} finally {
|
||||
if (fs.existsSync(tempFile)) {
|
||||
fs.unlinkSync(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!binaryData) {
|
||||
throw new Error('Failed to download binary from any source');
|
||||
}
|
||||
|
||||
// Write binary to destination
|
||||
const dir = path.dirname(destination);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(destination, binaryData, { mode: 0o755 });
|
||||
console.log('✓ Binary installed successfully');
|
||||
|
||||
// Cache the binary for future use
|
||||
if (useCache) {
|
||||
try {
|
||||
cacheBinary(destination, version, platformKey);
|
||||
console.log('✓ Binary cached for future installations');
|
||||
} catch (e) {
|
||||
// Caching failed, but installation succeeded
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to download binary: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
downloadBinary,
|
||||
downloadBinaryFromNpm,
|
||||
getAssetName
|
||||
};
|
||||
@ -1,30 +1,29 @@
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
// Platform mapping from Node.js to npm package names
|
||||
const PLATFORM_MAP = {
|
||||
// Tier 1 - Most common platforms
|
||||
'linux-x64': '@dagu-org/dagu-linux-x64',
|
||||
'linux-arm64': '@dagu-org/dagu-linux-arm64',
|
||||
'darwin-x64': '@dagu-org/dagu-darwin-x64',
|
||||
'darwin-arm64': '@dagu-org/dagu-darwin-arm64',
|
||||
'win32-x64': '@dagu-org/dagu-win32-x64',
|
||||
|
||||
"linux-x64": "dagu-linux-x64",
|
||||
"linux-arm64": "dagu-linux-arm64",
|
||||
"darwin-x64": "dagu-darwin-x64",
|
||||
"darwin-arm64": "dagu-darwin-arm64",
|
||||
"win32-x64": "dagu-win32-x64",
|
||||
|
||||
// Tier 2 - Common but less frequent
|
||||
'linux-ia32': '@dagu-org/dagu-linux-ia32',
|
||||
'win32-ia32': '@dagu-org/dagu-win32-ia32',
|
||||
'freebsd-x64': '@dagu-org/dagu-freebsd-x64',
|
||||
|
||||
"linux-ia32": "dagu-linux-ia32",
|
||||
"win32-ia32": "dagu-win32-ia32",
|
||||
"freebsd-x64": "dagu-freebsd-x64",
|
||||
|
||||
// Tier 3 - Rare platforms
|
||||
'win32-arm64': '@dagu-org/dagu-win32-arm64',
|
||||
'linux-ppc64': '@dagu-org/dagu-linux-ppc64',
|
||||
'linux-s390x': '@dagu-org/dagu-linux-s390x',
|
||||
'freebsd-arm64': '@dagu-org/dagu-freebsd-arm64',
|
||||
'freebsd-ia32': '@dagu-org/dagu-freebsd-ia32',
|
||||
'freebsd-arm': '@dagu-org/dagu-freebsd-arm',
|
||||
'openbsd-x64': '@dagu-org/dagu-openbsd-x64',
|
||||
'openbsd-arm64': '@dagu-org/dagu-openbsd-arm64',
|
||||
"win32-arm64": "dagu-win32-arm64",
|
||||
"linux-ppc64": "dagu-linux-ppc64",
|
||||
"linux-s390x": "dagu-linux-s390x",
|
||||
"freebsd-arm64": "dagu-freebsd-arm64",
|
||||
"freebsd-ia32": "dagu-freebsd-ia32",
|
||||
"freebsd-arm": "dagu-freebsd-arm",
|
||||
"openbsd-x64": "dagu-openbsd-x64",
|
||||
"openbsd-arm64": "dagu-openbsd-arm64",
|
||||
};
|
||||
|
||||
// Cache for binary path
|
||||
@ -36,37 +35,45 @@ let cachedBinaryPath = null;
|
||||
*/
|
||||
function getArmVariant() {
|
||||
// First try process.config
|
||||
if (process.config && process.config.variables && process.config.variables.arm_version) {
|
||||
if (
|
||||
process.config &&
|
||||
process.config.variables &&
|
||||
process.config.variables.arm_version
|
||||
) {
|
||||
return String(process.config.variables.arm_version);
|
||||
}
|
||||
|
||||
|
||||
// On Linux, check /proc/cpuinfo
|
||||
if (process.platform === 'linux') {
|
||||
if (process.platform === "linux") {
|
||||
try {
|
||||
const cpuinfo = fs.readFileSync('/proc/cpuinfo', 'utf8');
|
||||
|
||||
const cpuinfo = fs.readFileSync("/proc/cpuinfo", "utf8");
|
||||
|
||||
// Check for specific ARM architecture indicators
|
||||
if (cpuinfo.includes('ARMv6') || cpuinfo.includes('ARM926') || cpuinfo.includes('ARM1176')) {
|
||||
return '6';
|
||||
if (
|
||||
cpuinfo.includes("ARMv6") ||
|
||||
cpuinfo.includes("ARM926") ||
|
||||
cpuinfo.includes("ARM1176")
|
||||
) {
|
||||
return "6";
|
||||
}
|
||||
if (cpuinfo.includes('ARMv7') || cpuinfo.includes('Cortex-A')) {
|
||||
return '7';
|
||||
if (cpuinfo.includes("ARMv7") || cpuinfo.includes("Cortex-A")) {
|
||||
return "7";
|
||||
}
|
||||
|
||||
|
||||
// Check CPU architecture field
|
||||
const archMatch = cpuinfo.match(/^CPU architecture:\s*(\d+)/m);
|
||||
if (archMatch && archMatch[1]) {
|
||||
const arch = parseInt(archMatch[1], 10);
|
||||
if (arch >= 7) return '7';
|
||||
if (arch === 6) return '6';
|
||||
if (arch >= 7) return "7";
|
||||
if (arch === 6) return "6";
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors, fall through to default
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Default to ARMv7 (more common)
|
||||
return '7';
|
||||
return "7";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -76,32 +83,17 @@ function getArmVariant() {
|
||||
function getPlatformPackage() {
|
||||
let platform = process.platform;
|
||||
let arch = process.arch;
|
||||
|
||||
|
||||
// Special handling for ARM on Linux
|
||||
if (platform === 'linux' && arch === 'arm') {
|
||||
if (platform === "linux" && arch === "arm") {
|
||||
const variant = getArmVariant();
|
||||
return `@dagu-org/dagu-linux-armv${variant}`;
|
||||
return `dagu-linux-armv${variant}`;
|
||||
}
|
||||
|
||||
|
||||
const key = `${platform}-${arch}`;
|
||||
return PLATFORM_MAP[key] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported platforms list for error messages
|
||||
* @returns {string} Formatted list of supported platforms
|
||||
*/
|
||||
function getSupportedPlatforms() {
|
||||
const platforms = [
|
||||
'Linux: x64, arm64, arm (v6/v7), ia32, ppc64le, s390x',
|
||||
'macOS: x64 (Intel), arm64 (Apple Silicon)',
|
||||
'Windows: x64, ia32, arm64',
|
||||
'FreeBSD: x64, arm64, ia32, arm',
|
||||
'OpenBSD: x64, arm64'
|
||||
];
|
||||
return platforms.join('\n - ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the Dagu binary
|
||||
* @returns {string|null} Path to binary or null if not found
|
||||
@ -111,15 +103,17 @@ function getBinaryPath() {
|
||||
if (cachedBinaryPath && fs.existsSync(cachedBinaryPath)) {
|
||||
return cachedBinaryPath;
|
||||
}
|
||||
|
||||
const binaryName = process.platform === 'win32' ? 'dagu.exe' : 'dagu';
|
||||
|
||||
|
||||
const binaryName = process.platform === "win32" ? "dagu.exe" : "dagu";
|
||||
|
||||
// First, try platform-specific package
|
||||
const platformPackage = getPlatformPackage();
|
||||
if (platformPackage) {
|
||||
try {
|
||||
// Try to resolve the binary using require.resolve (Sentry approach)
|
||||
const binaryPath = require.resolve(`${platformPackage}/bin/${binaryName}`);
|
||||
const binaryPath = require.resolve(
|
||||
`${platformPackage}/bin/${binaryName}`
|
||||
);
|
||||
if (fs.existsSync(binaryPath)) {
|
||||
cachedBinaryPath = binaryPath;
|
||||
return binaryPath;
|
||||
@ -128,14 +122,14 @@ function getBinaryPath() {
|
||||
// Package not installed or binary not found
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fallback to local binary in main package
|
||||
const localBinary = path.join(__dirname, '..', 'bin', binaryName);
|
||||
const localBinary = path.join(__dirname, "..", "bin", binaryName);
|
||||
if (fs.existsSync(localBinary)) {
|
||||
cachedBinaryPath = localBinary;
|
||||
return localBinary;
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -147,24 +141,6 @@ function setPlatformBinary(binaryPath) {
|
||||
cachedBinaryPath = binaryPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform details for debugging
|
||||
* @returns {object} Platform information
|
||||
*/
|
||||
function getPlatformInfo() {
|
||||
return {
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
nodeVersion: process.version,
|
||||
v8Version: process.versions.v8,
|
||||
systemPlatform: os.platform(),
|
||||
systemArch: os.arch(),
|
||||
systemRelease: os.release(),
|
||||
armVariant: process.platform === 'linux' && process.arch === 'arm' ? getArmVariant() : null,
|
||||
detectedPackage: getPlatformPackage()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if platform-specific package is installed
|
||||
* @returns {boolean} True if installed, false otherwise
|
||||
@ -174,9 +150,9 @@ function isPlatformSpecificPackageInstalled() {
|
||||
if (!platformPackage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const binaryName = process.platform === 'win32' ? 'dagu.exe' : 'dagu';
|
||||
|
||||
|
||||
const binaryName = process.platform === "win32" ? "dagu.exe" : "dagu";
|
||||
|
||||
try {
|
||||
// Resolving will fail if the optionalDependency was not installed
|
||||
require.resolve(`${platformPackage}/bin/${binaryName}`);
|
||||
@ -186,45 +162,15 @@ function isPlatformSpecificPackageInstalled() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we're in a cross-platform scenario (node_modules moved between architectures)
|
||||
* @returns {object|null} Warning info if cross-platform detected
|
||||
*/
|
||||
function checkCrossPlatformScenario() {
|
||||
const pkg = require('../package.json');
|
||||
const optionalDeps = Object.keys(pkg.optionalDependencies || {});
|
||||
const currentPlatformPackage = getPlatformPackage();
|
||||
|
||||
// Check if any platform package is installed but it's not the right one
|
||||
for (const dep of optionalDeps) {
|
||||
try {
|
||||
require.resolve(`${dep}/package.json`);
|
||||
// Package is installed
|
||||
if (dep !== currentPlatformPackage) {
|
||||
// Wrong platform package is installed
|
||||
const installedPlatform = dep.replace('@dagu-org/dagu-', '');
|
||||
const currentPlatform = `${process.platform}-${process.arch}`;
|
||||
return {
|
||||
installed: installedPlatform,
|
||||
current: currentPlatform,
|
||||
message: `WARNING: Found binary for ${installedPlatform} but current platform is ${currentPlatform}.\nThis usually happens when node_modules are copied between different systems.\nPlease reinstall @dagu-org/dagu to get the correct binary.`
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// Package not installed, continue checking
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPlatformPackage,
|
||||
getBinaryPath,
|
||||
setPlatformBinary,
|
||||
getSupportedPlatforms,
|
||||
getPlatformInfo,
|
||||
getArmVariant,
|
||||
isPlatformSpecificPackageInstalled,
|
||||
checkCrossPlatformScenario
|
||||
};
|
||||
getPlatformInfo: () => ({
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
nodeVersion: process.version,
|
||||
detectedPackage: getPlatformPackage(),
|
||||
}),
|
||||
};
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
const { spawn } = require("child_process");
|
||||
const fs = require("fs");
|
||||
|
||||
/**
|
||||
* Validate that the binary is executable and returns expected output
|
||||
* @param {string} binaryPath Path to the binary
|
||||
* @returns {Promise<boolean>} True if valid, false otherwise
|
||||
*/
|
||||
async function validateBinary(binaryPath) {
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(binaryPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if file is executable (on Unix-like systems)
|
||||
if (process.platform !== "win32") {
|
||||
try {
|
||||
fs.accessSync(binaryPath, fs.constants.X_OK);
|
||||
} catch (e) {
|
||||
console.error("Binary is not executable");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to run the binary with version subcommand (Dagu uses 'version' not '--version')
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn(binaryPath, ["version"], {
|
||||
timeout: 5000, // 5 second timeout
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
proc.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
proc.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
proc.on("error", (error) => {
|
||||
console.error("Failed to execute binary:", error.message);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
proc.on("close", (code) => {
|
||||
// Check if the binary executed successfully and returned version info
|
||||
// Dagu version command returns version string to stderr like "1.18.0".
|
||||
if (code === 0 && stderr.trim().length > 0) {
|
||||
resolve(true);
|
||||
} else {
|
||||
console.error(`Binary validation failed: exit code ${code}`);
|
||||
if (stderr) {
|
||||
console.error("stderr:", stderr);
|
||||
}
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateBinary,
|
||||
};
|
||||
@ -25,10 +25,10 @@
|
||||
"license": "GPL-3.0",
|
||||
"author": "Dagu Contributors",
|
||||
"bin": {
|
||||
"dagu": "./bin/dagu"
|
||||
"dagu": "bin/cli"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "node install.js"
|
||||
"postinstall": "node ./install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"tar": "^7.4.3"
|
||||
@ -56,4 +56,4 @@
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user