wyvrnpm 1.2.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +513 -413
- package/bin/wyvrn.js +277 -163
- package/package.json +37 -37
- package/src/commands/install.js +156 -109
- package/src/commands/link.js +316 -0
- package/src/commands/profile.js +171 -0
- package/src/commands/publish.js +232 -180
- package/src/config.js +63 -60
- package/src/download.js +325 -164
- package/src/index.js +10 -9
- package/src/link-utils.js +95 -0
- package/src/manifest.js +133 -77
- package/src/profile.js +377 -0
- package/src/providers/base.js +133 -56
- package/src/providers/file.js +181 -71
- package/src/providers/http.js +268 -118
- package/src/providers/s3.js +273 -136
- package/src/resolve.js +262 -200
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a symlink/junction from linkPath pointing to targetPath.
|
|
8
|
+
* Uses junctions on Windows (no admin rights required), symlinks on Unix.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} targetPath - The actual directory to link to.
|
|
11
|
+
* @param {string} linkPath - The path where the link will be created.
|
|
12
|
+
*/
|
|
13
|
+
function createLink(targetPath, linkPath) {
|
|
14
|
+
const absoluteTarget = path.resolve(targetPath);
|
|
15
|
+
const absoluteLink = path.resolve(linkPath);
|
|
16
|
+
|
|
17
|
+
// Ensure parent directory exists
|
|
18
|
+
const parentDir = path.dirname(absoluteLink);
|
|
19
|
+
if (!fs.existsSync(parentDir)) {
|
|
20
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Remove existing link/directory if present
|
|
24
|
+
if (fs.existsSync(absoluteLink)) {
|
|
25
|
+
const stats = fs.lstatSync(absoluteLink);
|
|
26
|
+
if (stats.isSymbolicLink()) {
|
|
27
|
+
fs.unlinkSync(absoluteLink);
|
|
28
|
+
} else {
|
|
29
|
+
fs.rmSync(absoluteLink, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Use 'junction' on Windows (no admin required), 'dir' on Unix
|
|
34
|
+
const linkType = process.platform === 'win32' ? 'junction' : 'dir';
|
|
35
|
+
fs.symlinkSync(absoluteTarget, absoluteLink, linkType);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Checks if a path is a symlink or junction.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} linkPath - Path to check.
|
|
42
|
+
* @returns {boolean} True if it's a symlink/junction.
|
|
43
|
+
*/
|
|
44
|
+
function isLink(linkPath) {
|
|
45
|
+
try {
|
|
46
|
+
const stats = fs.lstatSync(linkPath);
|
|
47
|
+
return stats.isSymbolicLink();
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Removes a symlink/junction without affecting the target directory.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} linkPath - Path to the symlink/junction to remove.
|
|
57
|
+
* @returns {boolean} True if removed, false if it didn't exist or wasn't a link.
|
|
58
|
+
*/
|
|
59
|
+
function removeLink(linkPath) {
|
|
60
|
+
try {
|
|
61
|
+
const stats = fs.lstatSync(linkPath);
|
|
62
|
+
if (stats.isSymbolicLink()) {
|
|
63
|
+
fs.unlinkSync(linkPath);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Gets the target path of a symlink/junction.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} linkPath - Path to the symlink/junction.
|
|
76
|
+
* @returns {string|null} The target path, or null if not a link.
|
|
77
|
+
*/
|
|
78
|
+
function getLinkTarget(linkPath) {
|
|
79
|
+
try {
|
|
80
|
+
const stats = fs.lstatSync(linkPath);
|
|
81
|
+
if (stats.isSymbolicLink()) {
|
|
82
|
+
return fs.readlinkSync(linkPath);
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
createLink,
|
|
92
|
+
isLink,
|
|
93
|
+
removeLink,
|
|
94
|
+
getLinkTarget,
|
|
95
|
+
};
|
package/src/manifest.js
CHANGED
|
@@ -1,77 +1,133 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* @
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* @param {object}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/** Current manifest schema version emitted by v2 tooling. */
|
|
7
|
+
const SCHEMA_VERSION = 2;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Valid project kind values, matching the C# serialisation enum.
|
|
11
|
+
* @type {string[]}
|
|
12
|
+
*/
|
|
13
|
+
const VALID_KINDS = [
|
|
14
|
+
'ConsoleApp',
|
|
15
|
+
'StaticLib',
|
|
16
|
+
'DynamicLib',
|
|
17
|
+
'HeaderOnlyLib',
|
|
18
|
+
'WebApp',
|
|
19
|
+
'Utility',
|
|
20
|
+
'Service',
|
|
21
|
+
'TestProject',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns a blank v2 manifest object populated with sensible defaults.
|
|
26
|
+
*
|
|
27
|
+
* Build profile settings are NOT stored in wyvrn.json — they live in named
|
|
28
|
+
* profile files under the wyvrnpm config directory
|
|
29
|
+
* (%LOCALAPPDATA%\wyvrnpm\profiles\ on Windows).
|
|
30
|
+
*
|
|
31
|
+
* Dependencies can optionally carry per-package settings overrides that are
|
|
32
|
+
* merged on top of the active machine profile at install/publish time:
|
|
33
|
+
*
|
|
34
|
+
* "dependencies": {
|
|
35
|
+
* "zlib": "1.2.11",
|
|
36
|
+
* "openssl": { "version": "3.0.0", "settings": { "compiler.runtime": "static" } }
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* @param {string} name - Project name (typically the directory basename).
|
|
40
|
+
* @returns {object}
|
|
41
|
+
*/
|
|
42
|
+
function defaultManifest(name) {
|
|
43
|
+
return {
|
|
44
|
+
name,
|
|
45
|
+
version: '1.0.0.0',
|
|
46
|
+
schemaVersion: SCHEMA_VERSION,
|
|
47
|
+
description: '',
|
|
48
|
+
kind: 'ConsoleApp',
|
|
49
|
+
dependencies: {},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Normalise the raw `dependencies` field from a manifest into a consistent
|
|
55
|
+
* `{ name → { version, settings } }` map, regardless of the input format:
|
|
56
|
+
*
|
|
57
|
+
* • String value : "1.0.0" → { version: "1.0.0", settings: {} }
|
|
58
|
+
* • Object value : { version, settings? } → kept (settings defaults to {})
|
|
59
|
+
* • Array format : [{ Name, Version }] → converted
|
|
60
|
+
*
|
|
61
|
+
* @param {object|Array|undefined} rawDeps
|
|
62
|
+
* @returns {Record<string, { version: string, settings: object }>}
|
|
63
|
+
*/
|
|
64
|
+
function normalizeDependencies(rawDeps) {
|
|
65
|
+
if (!rawDeps || typeof rawDeps !== 'object') return {};
|
|
66
|
+
|
|
67
|
+
if (Array.isArray(rawDeps)) {
|
|
68
|
+
const result = {};
|
|
69
|
+
for (const d of rawDeps) {
|
|
70
|
+
const n = d.Name ?? d.name;
|
|
71
|
+
const v = d.Version ?? d.version;
|
|
72
|
+
if (n && v) result[n] = { version: v, settings: {} };
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = {};
|
|
78
|
+
for (const [n, value] of Object.entries(rawDeps)) {
|
|
79
|
+
if (typeof value === 'string') {
|
|
80
|
+
result[n] = { version: value, settings: {} };
|
|
81
|
+
} else if (value && typeof value === 'object') {
|
|
82
|
+
result[n] = {
|
|
83
|
+
version: value.version ?? value.Version ?? '',
|
|
84
|
+
settings: value.settings ?? {},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Reads and parses a wyvrn.json manifest from disk.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} manifestPath - Absolute or relative path to wyvrn.json.
|
|
95
|
+
* @returns {object} Parsed manifest data.
|
|
96
|
+
* @throws {Error} If the file does not exist or contains invalid JSON.
|
|
97
|
+
*/
|
|
98
|
+
function readManifest(manifestPath) {
|
|
99
|
+
const resolved = path.resolve(manifestPath);
|
|
100
|
+
if (!fs.existsSync(resolved)) {
|
|
101
|
+
throw new Error(`Manifest not found: ${resolved}`);
|
|
102
|
+
}
|
|
103
|
+
const raw = fs.readFileSync(resolved, 'utf8');
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(raw);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
throw new Error(`Failed to parse manifest at ${resolved}: ${err.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Serialises and writes manifest data to disk with 2-space indentation.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} manifestPath - Absolute or relative path to wyvrn.json.
|
|
115
|
+
* @param {object} data - Manifest object to serialise.
|
|
116
|
+
*/
|
|
117
|
+
function writeManifest(manifestPath, data) {
|
|
118
|
+
const resolved = path.resolve(manifestPath);
|
|
119
|
+
const dir = path.dirname(resolved);
|
|
120
|
+
if (!fs.existsSync(dir)) {
|
|
121
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
fs.writeFileSync(resolved, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
SCHEMA_VERSION,
|
|
128
|
+
VALID_KINDS,
|
|
129
|
+
defaultManifest,
|
|
130
|
+
normalizeDependencies,
|
|
131
|
+
readManifest,
|
|
132
|
+
writeManifest,
|
|
133
|
+
};
|
package/src/profile.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// OS / Arch mapping (Node → Conan-style names)
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const OS_MAP = {
|
|
12
|
+
win32: 'Windows',
|
|
13
|
+
linux: 'Linux',
|
|
14
|
+
darwin: 'Macos',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const ARCH_MAP = {
|
|
18
|
+
x64: 'x86_64',
|
|
19
|
+
ia32: 'x86',
|
|
20
|
+
arm: 'armv7',
|
|
21
|
+
arm64: 'armv8',
|
|
22
|
+
ppc64: 'ppc64le',
|
|
23
|
+
s390x: 's390x',
|
|
24
|
+
mips: 'mips',
|
|
25
|
+
mipsel: 'mipsel',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Helpers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
function tryExec(cmd) {
|
|
32
|
+
try {
|
|
33
|
+
return execSync(cmd, { stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 })
|
|
34
|
+
.toString()
|
|
35
|
+
.trim();
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function detectOs() {
|
|
42
|
+
return OS_MAP[process.platform] ?? process.platform;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function detectArch() {
|
|
46
|
+
return ARCH_MAP[process.arch] ?? process.arch;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Compiler detection (per platform)
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
function detectCompilerWindows() {
|
|
54
|
+
// 1) MSVC via vswhere
|
|
55
|
+
const vswhere = tryExec(
|
|
56
|
+
'"C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe"' +
|
|
57
|
+
' -latest -property installationVersion',
|
|
58
|
+
);
|
|
59
|
+
if (vswhere) {
|
|
60
|
+
const major = parseInt(vswhere.split('.')[0], 10);
|
|
61
|
+
// VS17 → MSVC 193x, VS16 → 192x, VS15 → 191x, VS14 → 190x
|
|
62
|
+
const vsToMsvc = { 17: '193', 16: '192', 15: '191', 14: '190' };
|
|
63
|
+
return {
|
|
64
|
+
compiler: 'msvc',
|
|
65
|
+
'compiler.version': vsToMsvc[major] ?? String(major),
|
|
66
|
+
'compiler.runtime': 'dynamic',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2) Clang / clang-cl
|
|
71
|
+
const clang = tryExec('clang --version');
|
|
72
|
+
if (clang) {
|
|
73
|
+
const m = clang.match(/version (\d+)/);
|
|
74
|
+
return {
|
|
75
|
+
compiler: 'clang',
|
|
76
|
+
'compiler.version': m ? m[1] : 'unknown',
|
|
77
|
+
'compiler.runtime': null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 3) MinGW GCC
|
|
82
|
+
const gcc = tryExec('gcc --version');
|
|
83
|
+
if (gcc) {
|
|
84
|
+
const m = gcc.match(/(\d+\.\d+\.\d+)/);
|
|
85
|
+
return {
|
|
86
|
+
compiler: 'gcc',
|
|
87
|
+
'compiler.version': m ? m[1].split('.')[0] : 'unknown',
|
|
88
|
+
'compiler.runtime': null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { compiler: 'unknown', 'compiler.version': 'unknown', 'compiler.runtime': null };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function detectCompilerMacos() {
|
|
96
|
+
const clang = tryExec('clang --version');
|
|
97
|
+
if (clang) {
|
|
98
|
+
const appleMatch = clang.match(/Apple clang version (\d+)/i);
|
|
99
|
+
if (appleMatch) {
|
|
100
|
+
return { compiler: 'apple-clang', 'compiler.version': appleMatch[1], 'compiler.runtime': null };
|
|
101
|
+
}
|
|
102
|
+
const llvmMatch = clang.match(/version (\d+)/);
|
|
103
|
+
return {
|
|
104
|
+
compiler: 'clang',
|
|
105
|
+
'compiler.version': llvmMatch ? llvmMatch[1] : 'unknown',
|
|
106
|
+
'compiler.runtime': null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { compiler: 'unknown', 'compiler.version': 'unknown', 'compiler.runtime': null };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function detectCompilerLinux() {
|
|
113
|
+
// Prefer GCC on Linux
|
|
114
|
+
const gcc = tryExec('gcc --version');
|
|
115
|
+
if (gcc) {
|
|
116
|
+
const m = gcc.match(/(\d+\.\d+\.\d+)/);
|
|
117
|
+
if (m) {
|
|
118
|
+
return {
|
|
119
|
+
compiler: 'gcc',
|
|
120
|
+
'compiler.version': m[1].split('.')[0],
|
|
121
|
+
'compiler.runtime': null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const clang = tryExec('clang --version');
|
|
127
|
+
if (clang) {
|
|
128
|
+
const m = clang.match(/version (\d+)/);
|
|
129
|
+
return {
|
|
130
|
+
compiler: 'clang',
|
|
131
|
+
'compiler.version': m ? m[1] : 'unknown',
|
|
132
|
+
'compiler.runtime': null,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { compiler: 'unknown', 'compiler.version': 'unknown', 'compiler.runtime': null };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Public API
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Auto-detect the current build environment profile.
|
|
145
|
+
* Mirrors Conan 2.0 profile structure.
|
|
146
|
+
*
|
|
147
|
+
* @returns {WyvrnProfile}
|
|
148
|
+
*/
|
|
149
|
+
function detectProfile() {
|
|
150
|
+
const compilerInfo =
|
|
151
|
+
process.platform === 'win32' ? detectCompilerWindows()
|
|
152
|
+
: process.platform === 'darwin' ? detectCompilerMacos()
|
|
153
|
+
: detectCompilerLinux();
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
os: detectOs(),
|
|
157
|
+
arch: detectArch(),
|
|
158
|
+
compiler: compilerInfo.compiler,
|
|
159
|
+
'compiler.version': compilerInfo['compiler.version'],
|
|
160
|
+
'compiler.cppstd': '20',
|
|
161
|
+
'compiler.runtime': compilerInfo['compiler.runtime'],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Compute a stable 16-char SHA256 prefix that identifies a unique build profile.
|
|
167
|
+
* Only non-null, non-wildcard fields contribute to the hash.
|
|
168
|
+
*
|
|
169
|
+
* @param {WyvrnProfile} profile
|
|
170
|
+
* @returns {string} 16-character lowercase hex string
|
|
171
|
+
*/
|
|
172
|
+
function hashProfile(profile) {
|
|
173
|
+
const significant = Object.fromEntries(
|
|
174
|
+
Object.entries(profile)
|
|
175
|
+
.filter(([, v]) => v !== null && v !== undefined && v !== '*' && v !== '')
|
|
176
|
+
.sort(([a], [b]) => a.localeCompare(b)),
|
|
177
|
+
);
|
|
178
|
+
return crypto
|
|
179
|
+
.createHash('sha256')
|
|
180
|
+
.update(JSON.stringify(significant))
|
|
181
|
+
.digest('hex')
|
|
182
|
+
.slice(0, 16);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Compute the SHA256 hash of a file or Buffer.
|
|
187
|
+
*
|
|
188
|
+
* @param {string|Buffer} source - Absolute path to a file, or a Buffer.
|
|
189
|
+
* @returns {string} Full 64-character lowercase hex SHA256.
|
|
190
|
+
*/
|
|
191
|
+
function sha256Of(source) {
|
|
192
|
+
const buf = Buffer.isBuffer(source) ? source : fs.readFileSync(source);
|
|
193
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Try to get the current git commit SHA in the given directory.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} [cwd] - Directory to run git in (defaults to process.cwd()).
|
|
200
|
+
* @returns {string|null}
|
|
201
|
+
*/
|
|
202
|
+
function getGitSha(cwd) {
|
|
203
|
+
const result = tryExec(`git -C "${cwd || '.'}" rev-parse HEAD`);
|
|
204
|
+
return result && /^[0-9a-f]{40}$/i.test(result) ? result : null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Try to get the remote origin URL for the current git repo.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} [cwd]
|
|
211
|
+
* @returns {string|null}
|
|
212
|
+
*/
|
|
213
|
+
function getGitRepo(cwd) {
|
|
214
|
+
return tryExec(`git -C "${cwd || '.'}" remote get-url origin`) || null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Format a profile for human-readable display (Conan-style).
|
|
219
|
+
*
|
|
220
|
+
* @param {WyvrnProfile} profile
|
|
221
|
+
* @returns {string}
|
|
222
|
+
*/
|
|
223
|
+
function formatProfile(profile) {
|
|
224
|
+
const fieldOrder = [
|
|
225
|
+
'os', 'arch', 'compiler', 'compiler.version', 'compiler.cppstd', 'compiler.runtime',
|
|
226
|
+
];
|
|
227
|
+
const lines = ['[settings]'];
|
|
228
|
+
for (const key of fieldOrder) {
|
|
229
|
+
const val = profile[key];
|
|
230
|
+
if (val !== null && val !== undefined) {
|
|
231
|
+
lines.push(`${key}=${val}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return lines.join('\n');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Merge a partial override into a base profile, ignoring null/undefined overrides.
|
|
239
|
+
*
|
|
240
|
+
* @param {WyvrnProfile} base
|
|
241
|
+
* @param {Partial<WyvrnProfile>} overrides
|
|
242
|
+
* @returns {WyvrnProfile}
|
|
243
|
+
*/
|
|
244
|
+
function mergeProfile(base, overrides) {
|
|
245
|
+
return {
|
|
246
|
+
...base,
|
|
247
|
+
...Object.fromEntries(Object.entries(overrides).filter(([, v]) => v != null)),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Profile file management
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Returns the directory where named build profiles are stored.
|
|
257
|
+
* Windows : %LOCALAPPDATA%\wyvrnpm\profiles
|
|
258
|
+
* Other : ~/.config/wyvrnpm/profiles
|
|
259
|
+
*
|
|
260
|
+
* @returns {string}
|
|
261
|
+
*/
|
|
262
|
+
function getProfilesDir() {
|
|
263
|
+
const base = process.env.LOCALAPPDATA
|
|
264
|
+
? path.join(process.env.LOCALAPPDATA, 'wyvrnpm')
|
|
265
|
+
: path.join(require('os').homedir(), '.config', 'wyvrnpm');
|
|
266
|
+
return path.join(base, 'profiles');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Read a named profile from the profiles directory.
|
|
271
|
+
* Looks for `{name}.json`, then `{name}` without extension.
|
|
272
|
+
*
|
|
273
|
+
* @param {string} name - Profile name (e.g. "default", "msvc_release").
|
|
274
|
+
* @returns {WyvrnProfile|null} Parsed profile, or null if not found.
|
|
275
|
+
*/
|
|
276
|
+
function readProfile(name) {
|
|
277
|
+
const dir = getProfilesDir();
|
|
278
|
+
for (const filename of [`${name}.json`, name]) {
|
|
279
|
+
const p = path.join(dir, filename);
|
|
280
|
+
if (fs.existsSync(p)) {
|
|
281
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); }
|
|
282
|
+
catch { return null; }
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Write a named profile to the profiles directory as `{name}.json`.
|
|
290
|
+
*
|
|
291
|
+
* @param {string} name
|
|
292
|
+
* @param {WyvrnProfile} profile
|
|
293
|
+
*/
|
|
294
|
+
function writeProfile(name, profile) {
|
|
295
|
+
const dir = getProfilesDir();
|
|
296
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
297
|
+
fs.writeFileSync(
|
|
298
|
+
path.join(dir, `${name}.json`),
|
|
299
|
+
JSON.stringify(profile, null, 2) + '\n',
|
|
300
|
+
'utf8',
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Delete a named profile file. Does nothing if the profile does not exist.
|
|
306
|
+
*
|
|
307
|
+
* @param {string} name
|
|
308
|
+
* @returns {boolean} true if a file was deleted.
|
|
309
|
+
*/
|
|
310
|
+
function deleteProfile(name) {
|
|
311
|
+
const dir = getProfilesDir();
|
|
312
|
+
for (const filename of [`${name}.json`, name]) {
|
|
313
|
+
const p = path.join(dir, filename);
|
|
314
|
+
if (fs.existsSync(p)) {
|
|
315
|
+
fs.unlinkSync(p);
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* List all profile names available in the profiles directory.
|
|
324
|
+
*
|
|
325
|
+
* @returns {string[]} Sorted list of profile names (without `.json` extension).
|
|
326
|
+
*/
|
|
327
|
+
function listProfiles() {
|
|
328
|
+
const dir = getProfilesDir();
|
|
329
|
+
if (!fs.existsSync(dir)) return [];
|
|
330
|
+
return fs
|
|
331
|
+
.readdirSync(dir)
|
|
332
|
+
.filter((f) => f.endsWith('.json'))
|
|
333
|
+
.map((f) => f.slice(0, -5))
|
|
334
|
+
.sort();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Load the active build profile by name. Falls back to auto-detection if the
|
|
339
|
+
* named profile file does not exist yet.
|
|
340
|
+
*
|
|
341
|
+
* @param {string} [name='default']
|
|
342
|
+
* @returns {{ profile: WyvrnProfile, fromFile: boolean }}
|
|
343
|
+
*/
|
|
344
|
+
function loadProfile(name = 'default') {
|
|
345
|
+
const stored = readProfile(name);
|
|
346
|
+
if (stored) return { profile: stored, fromFile: true };
|
|
347
|
+
return { profile: detectProfile(), fromFile: false };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
module.exports = {
|
|
351
|
+
// Detection
|
|
352
|
+
detectProfile,
|
|
353
|
+
hashProfile,
|
|
354
|
+
sha256Of,
|
|
355
|
+
getGitSha,
|
|
356
|
+
getGitRepo,
|
|
357
|
+
formatProfile,
|
|
358
|
+
mergeProfile,
|
|
359
|
+
// Profile file management
|
|
360
|
+
getProfilesDir,
|
|
361
|
+
readProfile,
|
|
362
|
+
writeProfile,
|
|
363
|
+
deleteProfile,
|
|
364
|
+
listProfiles,
|
|
365
|
+
loadProfile,
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* @typedef {{
|
|
370
|
+
* os: string,
|
|
371
|
+
* arch: string,
|
|
372
|
+
* compiler: string,
|
|
373
|
+
* 'compiler.version': string,
|
|
374
|
+
* 'compiler.cppstd': string,
|
|
375
|
+
* 'compiler.runtime': string|null,
|
|
376
|
+
* }} WyvrnProfile
|
|
377
|
+
*/
|