Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cli/check-requirements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export async function checkRequirements({verbose = false} = {}) {
let highestRequiredMocPkgId = '';
let rootDir = getRootDir();

let resolvedPackages = await resolvePackages();
for (let [name, version] of Object.entries(resolvedPackages)) {
let resolved = await resolvePackages();
for (let [name, version] of Object.entries(resolved.packages)) {
if (getDependencyType(version) === 'mops') {
let pkgId = getPackageId(name, version);
let depConfig = readConfig(path.join(rootDir, '.mops', pkgId, 'mops.toml'));
Expand Down
4 changes: 2 additions & 2 deletions cli/commands/install/sync-local-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import {getDependencyType, getRootDir} from '../../mops.js';
import {resolvePackages} from '../../resolve-packages.js';

export async function syncLocalCache({verbose = false} = {}) : Promise<Record<string, string>> {
let resolvedPackages = await resolvePackages();
let resolved = await resolvePackages();
let rootDir = getRootDir();

verbose && console.log('Syncing local cache...');

let installedDeps : Record<string, string> = {};

await Promise.all(Object.entries(resolvedPackages).map(([name, value]) => {
await Promise.all(Object.entries(resolved.packages).map(([name, value]) => {
let depType = getDependencyType(value);

if (depType === 'mops' || depType === 'github') {
Expand Down
22 changes: 17 additions & 5 deletions cli/commands/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ export async function sources({conflicts = 'ignore' as 'warning' | 'error' | 'ig
return [];
}

let resolvedPackages = await resolvePackages({conflicts});
let resolved = await resolvePackages({conflicts});

// sources
return Object.entries(resolvedPackages).map(([name, version]) => {
let sources : string[] = [];
let packageDirectories : Record<string, string> = {};
Object.entries(resolved.packages).forEach(([name, version]) => {
let depType = getDependencyType(version);

let pkgDir;
Expand Down Expand Up @@ -44,6 +45,17 @@ export async function sources({conflicts = 'ignore' as 'warning' | 'error' | 'ig
pkgBaseDir = pkgDir;
}

return `--package ${name} ${pkgBaseDir}`;
}).filter(x => x != null);
packageDirectories[name] = pkgDir;
sources.push(`--package ${name} ${pkgBaseDir}`); // TODO: escape args
});
Object.entries(resolved.overrides).forEach(([name, overrides]) => {
const directory = packageDirectories[name];
if (!directory) {
throw new Error(`Unknown directory for package: ${name}`);
}
Object.entries(overrides).forEach(([fromPackage, toPackage]) => {
sources.push(`--override ${directory} ${fromPackage} ${toPackage}`); // TODO: escape args
});
});
return sources;
}
53 changes: 44 additions & 9 deletions cli/integrity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {sha256} from '@noble/hashes/sha256';
import {bytesToHex} from '@noble/hashes/utils';
import {getDependencyType, getRootDir, readConfig} from './mops.js';
import {mainActor} from './api/actors.js';
import {resolvePackages} from './resolve-packages.js';
import {ResolvedPackages, resolvePackages} from './resolve-packages.js';
import {getPackageId} from './helpers/get-package-id.js';

type LockFileGeneric = {
Expand All @@ -31,7 +31,14 @@ type LockFileV3 = {
deps : Record<string, string>;
};

type LockFile = LockFileV1 | LockFileV2 | LockFileV3;
type LockFileV4 = {
version : 4;
mopsTomlDepsHash : string;
hashes : Record<string, Record<string, string>>;
resolved : ResolvedPackages,
};

type LockFile = LockFileV1 | LockFileV2 | LockFileV3 | LockFileV4;

export async function checkIntegrity(lock ?: 'check' | 'update' | 'ignore') {
let force = !!lock;
Expand Down Expand Up @@ -61,8 +68,8 @@ async function getFileHashesFromRegistry() : Promise<[string, [string, Uint8Arra
}

async function getResolvedMopsPackageIds() : Promise<string[]> {
let resolvedPackages = await resolvePackages();
let packageIds = Object.entries(resolvedPackages)
let resolved = await resolvePackages();
let packageIds = Object.entries(resolved.packages)
.filter(([_, version]) => getDependencyType(version) === 'mops')
.map(([name, version]) => getPackageId(name, version));
return packageIds;
Expand Down Expand Up @@ -143,14 +150,14 @@ export async function updateLockFile() {
return;
}

let resolvedDeps = await resolvePackages();
let resolved = await resolvePackages();

let fileHashes = await getFileHashesFromRegistry();

let lockFileJson : LockFileV3 = {
version: 3,
let lockFileJson : LockFileV4 = {
version: 4,
mopsTomlDepsHash: getMopsTomlDepsHash(),
deps: resolvedDeps,
resolved,
hashes: fileHashes.reduce((acc, [packageId, fileHashes]) => {
acc[packageId] = fileHashes.reduce((acc, [fileId, hash]) => {
acc[fileId] = bytesToHex(new Uint8Array(hash));
Expand Down Expand Up @@ -217,7 +224,7 @@ export async function checkLockFile(force = false) {
// V3: check locked deps (including GitHub and local packages)
if (lockFileJson.version === 3) {
let lockedDeps = {...lockFileJson.deps};
let resolvedDeps = await resolvePackages();
let resolvedDeps = (await resolvePackages()).packages;

for (let name of Object.keys(resolvedDeps)) {
if (lockedDeps[name] !== resolvedDeps[name]) {
Expand All @@ -230,6 +237,34 @@ export async function checkLockFile(force = false) {
}
}

// V4: check locked packages (including GitHub and local packages) and overrides
if (lockFileJson.version === 4) {
let locked = {...lockFileJson.resolved};
let resolved = await resolvePackages();

for (let name of Object.keys(resolved.packages)) {
if (locked.packages[name] !== resolved.packages[name]) {
console.error('Integrity check failed');
console.error(`Mismatched package ${name}`);
console.error(`Locked: ${locked.packages[name]}`);
console.error(`Actual: ${resolved.packages[name]}`);
process.exit(1);
}
}
for (let path of Object.keys(resolved.overrides)) {
// TODO: possibly improve error message
const lockedOverride = JSON.stringify(locked.overrides);
const resolvedOverride = JSON.stringify(resolved.overrides);
if (lockedOverride !== resolvedOverride) {
console.error('Integrity check failed');
console.error(`Mismatched override for directory ${path}`);
console.error(`Locked: ${lockedOverride}`);
console.error(`Actual: ${resolvedOverride}`);
process.exit(1);
}
}
}

// check number of packages
if (Object.keys(lockFileJson.hashes).length !== packageIds.length) {
console.error('Integrity check failed');
Expand Down
96 changes: 69 additions & 27 deletions cli/resolve-packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,45 @@ import {getDepCacheDir, getDepCacheName} from './cache.js';
import {getPackageId} from './helpers/get-package-id.js';
import {checkLockFileLight, readLockFile} from './integrity.js';

export async function resolvePackages({conflicts = 'ignore' as 'warning' | 'error' | 'ignore'} = {}) : Promise<Record<string, string>> {
export interface ResolvedPackages {
packages : Record<string, string[]>,
overrides : Record<string, Record<string, string>>;
}

export async function resolvePackages({conflicts = 'ignore' as 'warning' | 'error' | 'ignore'} = {}) : Promise<ResolvedPackages> {
if (!checkConfigFile()) {
return {};
return {
packages: {},
overrides: {},
};
}

if (checkLockFileLight()) {
let lockFileJson = readLockFile();

if (lockFileJson && lockFileJson.version === 3) {
return lockFileJson.deps;
if (lockFileJson) {
if (lockFileJson.version === 3) {
return {
packages: Object.fromEntries(
Object.entries(lockFileJson.deps)
.map(version => [version])
),
overrides: {},
};
}
if (lockFileJson.version === 4) {
return lockFileJson.resolved;
}
}
}

let rootDir = getRootDir();
let packages : Record<string, Dependency & {isRoot : boolean;}> = {};
let packages : Record<string, Record<string, Dependency & {isRoot : boolean;}>> = {};
let versions : Record<string, Array<{
isMopsPackage : boolean;
name : string;
version : string;
dependencyOf : string;
isMopsPackage : boolean;
}>> = {};

let compareVersions = (a : string = '0.0.0', b : string = '0.0.0') => {
Expand All @@ -46,6 +66,7 @@ export async function resolvePackages({conflicts = 'ignore' as 'warning' | 'erro
};

const gitVerRegex = new RegExp(/v(\d{1,2}\.\d{1,2}\.\d{1,2})(-.*)?$/);
const rootName = '<root>';

const compareGitVersions = (repoA : string, repoB : string) => {
const {branch: a} = parseGithubURL(repoA);
Expand All @@ -70,20 +91,24 @@ export async function resolvePackages({conflicts = 'ignore' as 'warning' | 'erro
for (const pkgDetails of allDeps) {
const {name, repo, version} = pkgDetails;

const versionKey = version ? version.substring(0, version.indexOf('.')) : ''; // major version

const pkg = packages[name]?.[versionKey];

// take root dep version or bigger one
if (
isRoot
|| !packages[name]
|| !packages[name]?.isRoot
|| !pkg
|| !pkg?.isRoot
&& (
repo && packages[name]?.repo && compareGitVersions(packages[name]?.repo || '', repo) === -1
|| compareVersions(packages[name]?.version, version) === -1)
repo && pkg.repo && compareGitVersions(pkg.repo || '', repo) === -1
|| compareVersions(pkg.version, version) === -1)
) {
let temp = {
...pkgDetails,
isRoot,
};
packages[name] = temp;
(packages[name] || (packages[name] = {}))[versionKey] = temp;

// normalize path relative to the root config dir
if (pkgDetails.path) {
Expand All @@ -103,7 +128,7 @@ export async function resolvePackages({conflicts = 'ignore' as 'warning' | 'erro
localNestedDir = path.resolve(configDir, pkgDetails.path).replaceAll('{MOPS_ENV}', process.env.MOPS_ENV || 'local');
let mopsToml = path.join(localNestedDir, 'mops.toml');
if (existsSync(mopsToml)) {
nestedConfig = readConfig(mopsToml);
nestedConfig = readConfig();
}
}
else if (version) {
Expand All @@ -116,25 +141,24 @@ export async function resolvePackages({conflicts = 'ignore' as 'warning' | 'erro
await collectDeps(nestedConfig, localNestedDir, false);
}

if (!versions[name]) {
versions[name] = [];
}

let parentPkgId = isRoot ? '<root>' : '';
let parentPkgId = isRoot ? rootName : '';
if ('package' in config) {
parentPkgId = getPackageId(config.package?.name || '', config.package?.version || '');
}

const packageVersions = versions[name] || (versions[name] = []);
if (repo) {
const {branch} = parseGithubURL(repo);
versions[name]?.push({
packageVersions.push({
name,
version: branch,
dependencyOf: parentPkgId,
isMopsPackage: false,
});
}
else if (version) {
versions[name]?.push({
packageVersions.push({
name,
version: version,
dependencyOf: parentPkgId,
isMopsPackage: true,
Expand Down Expand Up @@ -168,9 +192,13 @@ export async function resolvePackages({conflicts = 'ignore' as 'warning' | 'erro
process.exit(1);
}

return Object.fromEntries(
Object.entries(packages).map(([name, pkg]) => {
let version : string;
let resolved : ResolvedPackages = {
packages: {},
overrides: {},
};
Object.entries(packages).forEach(([name, packageMap]) => {
Object.entries(packageMap).forEach(([_key, pkg]) => {
let version : string | undefined;
if (pkg.path) {
version = path.resolve(rootDir, pkg.path).replaceAll('{MOPS_ENV}', process.env.MOPS_ENV || 'local');
}
Expand All @@ -180,10 +208,24 @@ export async function resolvePackages({conflicts = 'ignore' as 'warning' | 'erro
else if (pkg.version) {
version = pkg.version;
}
else {
return [name, ''];

if (version) {
(resolved.packages[name] || (resolved.packages[name] = [])).push(version);
}
return [name, version];
}).filter(([, version]) => version !== '')
);
});
});
Object.entries(versions).forEach(([_name, versionedPackages]) => {
let visitedVersions = new Set<string>();
for (let pkg of versionedPackages) {
if (pkg.dependencyOf === rootName) {
return;
}
if (visitedVersions.has(pkg.version)) {
continue;
}
visitedVersions.add(pkg.version);
}
// resolved.overrides[name] =
});
return resolved;
}
Loading