Skip to content
Merged
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
44 changes: 44 additions & 0 deletions src/cli/services/path.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,50 @@ describe('cli/services/path.service', () => {
);
});

test('createSymlink', async () => {
const target = rootPath('env123/target');
const link = rootPath('env123/link');
await pathSvc.createDir(rootPath('env123'));
await writeFile(target, '');

expect(await pathSvc.createSymlink(target, link)).toBeUndefined();

expect(await fs.readlink(link)).toBe(target);
});

test('createSymlink: keeps an existing link', async () => {
const target = rootPath('env123/target');
const link = rootPath('env123/link');
await pathSvc.createDir(rootPath('env123'));
await writeFile(target, '');
await pathSvc.createSymlink(target, link);

await expect(pathSvc.createSymlink(target, link)).resolves.toBeUndefined();

expect(await fs.readlink(link)).toBe(target);
});

test('createSymlink: keeps an existing folder', async () => {
const target = rootPath('env123/target');
const dir = rootPath('env123/dir');
await pathSvc.createDir(dir);

await pathSvc.createSymlink(target, dir);

expect((await stat(dir)).isDirectory()).toBe(true);
});

test('createSymlink: keeps a dangling link', async () => {
const target = rootPath('env123/missing');
const link = rootPath('env123/link');
await pathSvc.createDir(rootPath('env123'));
await fs.symlink(target, link);

await pathSvc.createSymlink(rootPath('env123/other'), link);

expect(await fs.readlink(link)).toBe(target);
});

test('toolInit', async () => {
expect(pathSvc.toolInitPath('node')).toBe(
rootPath('tmp/containerbase/tool.init.d/node'),
Expand Down
13 changes: 13 additions & 0 deletions src/cli/services/path.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,19 @@ export class PathService {
await this.setOwner({ path, mode });
}

/**
* Creates a symlink at `path` pointing to `target`. An existing file,
* folder or link at `path` is left as is.
*/
async createSymlink(target: string, path: string): Promise<void> {
const stats = await fs.lstat(path).catch(() => null);
if (stats) {
logger.debug({ path }, 'path exists, skipping symlink');
return;
}
await fs.symlink(target, path);
Comment thread
viceice marked this conversation as resolved.
}

/** Creates the tool path and returns it. */
async createToolPath(tool: string): Promise<string> {
const toolPath = this.toolPath(tool);
Expand Down
22 changes: 9 additions & 13 deletions src/cli/tools/dart/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,19 @@ import type { EnvService, PathService } from '../../services';
import { pathExists } from '../../utils/index.ts';

/**
* Links the user's `.dart` and `.dart-tool` folders to the cache, and turns
* off the dart analytics for root.
* Links the user's `.dart` and `.dart-tool` folders to the cache, keeping any
* existing link, and turns off the dart analytics for root.
*/
export async function prepareDartHome(
envSvc: EnvService,
pathSvc: PathService,
): Promise<void> {
const dart = join(envSvc.userHome, '.dart');
if (!(await pathExists(dart))) {
await fs.symlink(join(pathSvc.cachePath, '.dart'), dart);
await fs.symlink(
join(pathSvc.cachePath, '.dart-tool'),
join(envSvc.userHome, '.dart-tool'),
);
}
await pathSvc.createSymlink(join(pathSvc.cachePath, '.dart'), dart);
await pathSvc.createSymlink(
join(pathSvc.cachePath, '.dart-tool'),
join(envSvc.userHome, '.dart-tool'),
);

// for root
const rootDart = join(envSvc.rootDir, 'root', '.dart');
Expand Down Expand Up @@ -52,15 +50,13 @@ export async function initDartHome(pathSvc: PathService): Promise<void> {
}
}

/** Links the user's `.pub-cache` folder to the cache. */
/** Links the user's `.pub-cache` folder to the cache, keeping any existing link. */
export async function preparePubCache(
envSvc: EnvService,
pathSvc: PathService,
): Promise<void> {
const pubCache = join(envSvc.userHome, '.pub-cache');
if (!(await pathExists(pubCache))) {
await fs.symlink(join(pathSvc.cachePath, '.pub-cache'), pubCache);
}
await pathSvc.createSymlink(join(pathSvc.cachePath, '.pub-cache'), pubCache);
}

/** Creates the `.pub-cache` folder in the cache. */
Expand Down
5 changes: 4 additions & 1 deletion src/cli/tools/docker/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('cli/tools/docker/index', () => {

expect(execaMock).toHaveBeenCalledWith(
'groupadd',
['-g', '999', 'docker'],
['-f', '-g', '999', 'docker'],
expect.any(Object),
);
expect(execaMock).toHaveBeenCalledWith(
Expand All @@ -114,6 +114,9 @@ describe('cli/tools/docker/index', () => {
join(envSvc.rootDir, 'usr/local/lib/docker', 'cli-plugins'),
),
).toBe(join(pathSvc.cachePath, '.docker', 'cli-plugins'));

// second run is a no-op, it would throw on existing symlinks otherwise
await expect(svc.prepare()).resolves.toBeUndefined();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});
});
11 changes: 6 additions & 5 deletions src/cli/tools/docker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@ export class DockerPrepareService extends BasePrepareService {
readonly name = 'docker';

/**
* Adds the user to a `docker` group, and links the user's `.docker` folder
* and the global cli plugins folder to the cache.
* Adds the user to a `docker` group, creating it if it doesn't already
* exist, and links the user's `.docker` folder and the global cli plugins
* folder to the cache, keeping any existing link.
*/
override async prepare(): Promise<void> {
await this._spawn('groupadd', ['-g', '999', 'docker']);
await this._spawn('groupadd', ['-f', '-g', '999', 'docker']);
await this._spawn('usermod', ['-aG', 'docker', this.envSvc.userName]);
const globalDocker = join(this.envSvc.rootDir, 'usr/local/lib/docker');
await fs.mkdir(globalDocker, { recursive: true });
await fs.symlink(
await this.pathSvc.createSymlink(
join(this.pathSvc.cachePath, '.docker'),
join(this.envSvc.userHome, '.docker'),
);
await fs.symlink(
await this.pathSvc.createSymlink(
join(this.pathSvc.cachePath, '.docker', 'cli-plugins'),
join(globalDocker, 'cli-plugins'),
);
Expand Down
3 changes: 3 additions & 0 deletions src/cli/tools/dotnet/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ describe('cli/tools/dotnet/index', () => {
).toContain(
'export DOTNET_CLI_TELEMETRY_OPTOUT=${DOTNET_CLI_TELEMETRY_OPTOUT-1}',
);

// second run is a no-op, it would throw on the existing symlink otherwise
await expect(svc.prepare()).resolves.toBeUndefined();
});

test('initialize is idempotent', async () => {
Expand Down
5 changes: 2 additions & 3 deletions src/cli/tools/dotnet/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import fs from 'node:fs/promises';
import { join } from 'node:path';
import { inject, injectFromHierarchy, injectable } from 'inversify';
import { BaseInstallService } from '../../install-tool/base-install.service.ts';
Expand All @@ -16,7 +15,7 @@ export class DotnetPrepareService extends BasePrepareService {

/**
* Installs the apt packages dotnet needs on jammy and noble, initializes the
* cache and links `~/.nuget` to it.
* cache and links `~/.nuget` to it, keeping any existing link.
*/
override async prepare(): Promise<void> {
const distro = await getDistro();
Expand Down Expand Up @@ -47,7 +46,7 @@ export class DotnetPrepareService extends BasePrepareService {
}

await this.initialize();
await fs.symlink(
await this.pathSvc.createSymlink(
join(this.pathSvc.cachePath, '.nuget'),
join(this.envSvc.userHome, '.nuget'),
);
Expand Down
12 changes: 7 additions & 5 deletions src/cli/tools/dotnet/mono.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ export class MonoPrepareService extends BasePrepareService {
readonly name = 'mono';

/**
* Initializes the cache, links `~/.mono` to it, and sets up the mono cert
* store with a hook that syncs it on ca certificate updates.
* Initializes the cache, links `~/.mono` to it, keeping any existing link,
* and sets up the mono cert store with a hook that syncs it on ca
* certificate updates.
*/
override async prepare(): Promise<void> {
// TODO: install mono dependencies if needed
await this.initialize();
const mono = join(this.envSvc.userHome, '.mono');
if (!(await pathExists(mono))) {
await fs.symlink(join(this.pathSvc.cachePath, '.mono'), mono);
}
await this.pathSvc.createSymlink(
join(this.pathSvc.cachePath, '.mono'),
mono,
);

const src = join(this.pathSvc.sslPath, 'mono');
if (!(await pathExists(src))) {
Expand Down
3 changes: 3 additions & 0 deletions src/cli/tools/flutter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ describe('cli/tools/flutter', () => {
expect(
await fs.readlink(join(envSvc.userHome, '.flutter_tool_state')),
).toBe(join(pathSvc.cachePath, '.flutter_tool_state'));

// second run is a no-op, it would throw on existing symlinks otherwise
await expect(svc.prepare()).resolves.toBeUndefined();
});
});
});
9 changes: 5 additions & 4 deletions src/cli/tools/flutter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ export class FlutterPrepareService extends BasePrepareService {
readonly name = 'flutter';

/**
* Initializes the cache, links the dart and pub cache folders, and turns
* off analytics and the welcome message for root and the user.
* Initializes the cache, links the dart and pub cache folders, keeping any
* existing link, and turns off analytics and the welcome message for root
* and the user.
*/
override async prepare(): Promise<void> {
await this.initialize();
Expand All @@ -31,12 +32,12 @@ export class FlutterPrepareService extends BasePrepareService {
);

// for user
await fs.symlink(
await this.pathSvc.createSymlink(
join(this.pathSvc.cachePath, '.flutter'),
join(this.envSvc.userHome, '.flutter'),
);

await fs.symlink(
await this.pathSvc.createSymlink(
join(this.pathSvc.cachePath, '.flutter_tool_state'),
join(this.envSvc.userHome, '.flutter_tool_state'),
);
Expand Down
11 changes: 6 additions & 5 deletions src/cli/tools/java/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export class JavaPrepareService extends BasePrepareService {

/**
* Initializes the cache, links the maven, gradle and android folders to it,
* and sets up the shared cacerts from the latest lts jre, once.
* keeping any existing link, and sets up the shared cacerts from the
* latest lts jre, once.
*
* @throws when the latest jre can't be resolved
*/
Expand All @@ -39,19 +40,19 @@ export class JavaPrepareService extends BasePrepareService {
}

// compatibility with gradle and maven
await fs.symlink(
await this.pathSvc.createSymlink(
path.join(this.pathSvc.cachePath, '.m2'),
path.join(this.envSvc.userHome, '.m2'),
);
await fs.symlink(
await this.pathSvc.createSymlink(
path.join(this.pathSvc.cachePath, '.gradle'),
path.join(this.envSvc.userHome, '.gradle'),
);
await fs.symlink(
await this.pathSvc.createSymlink(
path.join(this.pathSvc.cachePath, '.android'),
path.join(this.envSvc.userHome, '.android'),
);
await fs.symlink(
await this.pathSvc.createSymlink(
path.join(this.pathSvc.cachePath, '.android-sdk'),
path.join(this.envSvc.userHome, '.android-sdk'),
);
Expand Down
3 changes: 3 additions & 0 deletions src/cli/tools/node/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,9 @@ describe('cli/tools/node/utils', () => {
expect(await fs.readlink(join(envSvc.userHome, '.npmrc'))).toBe(
join(pathSvc.cachePath, '.npmrc'),
);

// second run is a no-op
await expect(prepareSymlinks(envSvc, pathSvc)).resolves.toBeUndefined();
});

test('prepareGlobalConfig', async () => {
Expand Down
9 changes: 6 additions & 3 deletions src/cli/tools/node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,16 +316,19 @@ export async function prepareNpmrc(pathSvc: PathService): Promise<void> {
}
}

/** Links the user's `.npm` folder and `.npmrc` to the cache. */
/**
* Links the user's `.npm` folder and `.npmrc` to the cache, keeping any
* existing link.
*/
export async function prepareSymlinks(
envSvc: EnvService,
pathSvc: PathService,
): Promise<void> {
await fs.symlink(
await pathSvc.createSymlink(
join(pathSvc.cachePath, '.npm'),
join(envSvc.userHome, '.npm'),
);
await fs.symlink(
await pathSvc.createSymlink(
join(pathSvc.cachePath, '.npmrc'),
join(envSvc.userHome, '.npmrc'),
);
Expand Down
3 changes: 3 additions & 0 deletions src/cli/tools/python/conan.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ describe('cli/tools/python/conan', () => {
expect(await fs.readlink(join(envSvc.userHome, '.conan2'))).toBe(
join(pathSvc.cachePath, '.conan2'),
);

// second run is a no-op, it would throw on the existing symlink otherwise
await expect(svc.prepare()).resolves.toBeUndefined();
},
);

Expand Down
5 changes: 2 additions & 3 deletions src/cli/tools/python/conan.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import fs from 'node:fs/promises';
import { join } from 'node:path';
import { inject, injectFromHierarchy, injectable } from 'inversify';
import { BasePrepareService } from '../../prepare-tool/base-prepare.service.ts';
Expand All @@ -17,14 +16,14 @@ export class ConanPrepareService extends BasePrepareService {

/**
* Installs the build tools conan needs, initializes the cache and links
* `~/.conan2` to it.
* `~/.conan2` to it, keeping any existing link.
*/
override async prepare(): Promise<void> {
await this.aptSvc.install('cmake', 'gcc', 'g++', 'make', 'perl');

await this.initialize();

await fs.symlink(
await this.pathSvc.createSymlink(
join(this.pathSvc.cachePath, '.conan2'),
join(this.envSvc.userHome, '.conan2'),
);
Expand Down