Skip to content

Commit

Permalink
feat(cli): convert dotnet (#1347)
Browse files Browse the repository at this point in the history
  • Loading branch information
viceice authored Aug 23, 2023
1 parent efb532e commit 025ba94
Show file tree
Hide file tree
Showing 9 changed files with 299 additions and 93 deletions.
2 changes: 2 additions & 0 deletions src/cli/install-tool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Container, injectable } from 'inversify';
import { rootContainer } from '../services';
import { InstallDartService } from '../tools/dart';
import { InstallDockerService } from '../tools/docker';
import { InstallDotnetService } from '../tools/dotnet';
import { InstallFluxService } from '../tools/flux';
import { InstallNodeService } from '../tools/node';
import {
Expand Down Expand Up @@ -35,6 +36,7 @@ function prepareContainer(): Container {
container.bind(INSTALL_TOOL_TOKEN).to(InstallCorepackService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallDartService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallDockerService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallDotnetService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallFluxService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallLernaService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallNodeService);
Expand Down
4 changes: 3 additions & 1 deletion src/cli/prepare-tool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Container } from 'inversify';
import { rootContainer } from '../services';
import { PrepareDartService } from '../tools/dart';
import { PrepareDockerService } from '../tools/docker';
import { PrepareDotnetService } from '../tools/dotnet';
import { logger } from '../utils';
import { PrepareLegacyToolsService } from './prepare-legacy-tools.service';
import { PREPARE_TOOL_TOKEN, PrepareToolService } from './prepare-tool.service';
Expand All @@ -16,8 +17,9 @@ function prepareContainer(): Container {
container.bind(PrepareLegacyToolsService).toSelf();

// tool services
container.bind(PREPARE_TOOL_TOKEN).to(PrepareDockerService);
container.bind(PREPARE_TOOL_TOKEN).to(PrepareDartService);
container.bind(PREPARE_TOOL_TOKEN).to(PrepareDotnetService);
container.bind(PREPARE_TOOL_TOKEN).to(PrepareDockerService);

logger.trace('preparing container done');
return container;
Expand Down
56 changes: 56 additions & 0 deletions src/cli/services/apt.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { env } from 'node:process';
import type { Container } from 'inversify';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { AptService, rootContainer } from '.';

const mocks = vi.hoisted(() => ({
execa: vi.fn(),
rm: vi.fn(),
writeFile: vi.fn(),
}));

vi.mock('execa', () => mocks);
vi.mock('node:fs/promises', async () => ({
default: { ...(await vi.importActual<any>('node:fs/promises')), ...mocks },
...mocks,
}));

describe('apt.service', () => {
let child!: Container;

beforeEach(() => {
child = rootContainer.createChild();
delete env.APT_HTTP_PROXY;
});

test('skips install', async () => {
const svc = child.get(AptService);

mocks.execa.mockResolvedValueOnce({
stdout: 'Status: install ok installed',
});
await svc.install('some-pkg');
expect(mocks.execa).toHaveBeenCalledTimes(1);
});

test('works', async () => {
const svc = child.get(AptService);

mocks.execa.mockRejectedValueOnce(new Error('not installed'));
await svc.install('some-pkg');
expect(mocks.execa).toHaveBeenCalledTimes(3);
expect(mocks.writeFile).not.toHaveBeenCalled();
expect(mocks.rm).not.toHaveBeenCalled();
});

test('uses proxy', async () => {
env.APT_HTTP_PROXY = 'http://proxy';
const svc = child.get(AptService);

mocks.execa.mockRejectedValueOnce(new Error('not installed'));
await svc.install('some-pkg', 'other-pkg');
expect(mocks.execa).toHaveBeenCalledTimes(4);
expect(mocks.writeFile).toHaveBeenCalledOnce();
expect(mocks.rm).toHaveBeenCalledOnce();
});
});
66 changes: 66 additions & 0 deletions src/cli/services/apt.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { rm, writeFile } from 'fs/promises';
import { join } from 'node:path';
import { execa } from 'execa';
import { inject, injectable } from 'inversify';
import { logger } from '../utils';
import { EnvService } from './env.service';

@injectable()
export class AptService {
constructor(@inject(EnvService) private readonly envSvc: EnvService) {}

async install(...packages: string[]): Promise<void> {
const todo: string[] = [];

for (const pkg of packages) {
if (await this.isInstalled(pkg)) {
continue;
}
todo.push(pkg);
}

if (todo.length === 0) {
logger.debug({ packages }, 'all packages already installed');
return;
}

logger.debug({ packages: todo }, 'installing packages');

if (this.envSvc.aptProxy) {
logger.debug({ proxy: this.envSvc.aptProxy }, 'using apt proxy');
await writeFile(
join(
this.envSvc.rootDir,
'etc/apt/apt.conf.d/containerbase-proxy.conf',
),
`Acquire::http::Proxy "${this.envSvc.aptProxy}";\n`,
);
}

try {
await execa('apt-get', ['-qq', 'update']);
await execa('apt-get', ['-qq', 'install', '-y', ...todo]);
} finally {
if (this.envSvc.aptProxy) {
await rm(
join(
this.envSvc.rootDir,
'etc/apt/apt.conf.d/containerbase-proxy.conf',
),
{
force: true,
},
);
}
}
}

private async isInstalled(pkg: string): Promise<boolean> {
try {
const res = await execa('dpkg', ['-s', pkg]);
return res.stdout.includes('Status: install ok installed');
} catch {
return false;
}
}
}
5 changes: 4 additions & 1 deletion src/cli/services/compression.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export class CompressionService {
return;
}

await pipeline(createReadStream(file), tar.x({ cwd, strip }, files));
await pipeline(
createReadStream(file),
tar.x({ cwd, strip, newer: true, keep: false }, files),
);
}
}
4 changes: 4 additions & 0 deletions src/cli/services/env.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export class EnvService {
}
}

get aptProxy(): string | null {
return env.APT_HTTP_PROXY ?? null;
}

get cacheDir(): string | null {
return env.CONTAINERBASE_CACHE_DIR ?? null;
}
Expand Down
11 changes: 7 additions & 4 deletions src/cli/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { Container } from 'inversify';
import { AptService } from './apt.service';
import { CompressionService } from './compression.service';
import { EnvService } from './env.service';
import { HttpService } from './http.service';
import { PathService } from './path.service';
import { VersionService } from './version.service';

export {
AptService,
CompressionService,
EnvService,
HttpService,
PathService,
VersionService,
HttpService,
CompressionService,
};

export const rootContainer = new Container();

rootContainer.bind(AptService).toSelf();
rootContainer.bind(CompressionService).toSelf();
rootContainer.bind(EnvService).toSelf();
rootContainer.bind(HttpService).toSelf();
rootContainer.bind(PathService).toSelf();
rootContainer.bind(VersionService).toSelf();
rootContainer.bind(HttpService).toSelf();
rootContainer.bind(CompressionService).toSelf();
157 changes: 157 additions & 0 deletions src/cli/tools/dotnet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { chmod, chown, mkdir } from 'node:fs/promises';
import { join } from 'node:path';
import { execa } from 'execa';
import { inject, injectable } from 'inversify';
import { InstallToolBaseService } from '../../install-tool/install-tool-base.service';
import { PrepareToolBaseService } from '../../prepare-tool/prepare-tool-base.service';
import {
AptService,
CompressionService,
EnvService,
HttpService,
PathService,
} from '../../services';
import { getDistro, parse } from '../../utils';

@injectable()
export class PrepareDotnetService extends PrepareToolBaseService {
readonly name = 'dotnet';

constructor(
@inject(EnvService) private readonly envSvc: EnvService,
@inject(AptService) private readonly aptSvc: AptService,
) {
super();
}

async execute(): Promise<void> {
const distro = await getDistro();

switch (distro.versionCode) {
case 'focal':
await this.aptSvc.install(
'libc6',
'libgcc1',
'libgssapi-krb5-2',
'libicu66',
'libssl1.1',
'libstdc++6',
'zlib1g',
);
break;
case 'jammy':
await this.aptSvc.install(
'libc6',
'libgcc1',
'libgssapi-krb5-2',
'libicu70',
'libssl3',
'libstdc++6',
'zlib1g',
);
break;
}

const nuget = join(this.envSvc.userHome, '.nuget');
await mkdir(nuget);
await chown(nuget, this.envSvc.userId, 0);
await chmod(nuget, 0o775);
}
}

@injectable()
export class InstallDotnetService extends InstallToolBaseService {
readonly name = 'dotnet';

private get arch(): string {
switch (this.envSvc.arch) {
case 'arm64':
return 'arm64';
case 'amd64':
return 'x64';
}
}

constructor(
@inject(EnvService) envSvc: EnvService,
@inject(PathService) pathSvc: PathService,
@inject(HttpService) private http: HttpService,
@inject(CompressionService) private compress: CompressionService,
) {
super(pathSvc, envSvc);
}

override isInstalled(version: string): Promise<boolean> {
const toolPath = this.pathSvc.toolPath(this.name);
return this.pathSvc.fileExists(join(toolPath, 'sdk', version, '.version'));
}

override async install(version: string): Promise<void> {
const toolPath = this.pathSvc.toolPath(this.name);

// https://dotnetcli.azureedge.net/dotnet/Sdk/6.0.413/dotnet-sdk-6.0.413-linux-x64.tar.gz
const url = `https://dotnetcli.azureedge.net/dotnet/Sdk/${version}/dotnet-sdk-${version}-linux-${this.arch}.tar.gz`;
const file = await this.http.download({ url });

await this.compress.extract({
file,
cwd: toolPath,
strip: 1,
});

// we need write access to some sub dirs for non root
if (this.envSvc.isRoot) {
// find "$tool_path" -type d -exec chmod g+w {} \;
await execa('find', [
toolPath,
'-type',
'd',
'-exec',
'chmod',
'g+w',
'{}',
';',
]);
}
}

override async link(version: string): Promise<void> {
const src = this.pathSvc.toolPath(this.name);
await this.shellwrapper({ srcDir: src });

await execa('dotnet', ['new']);
if (this.envSvc.isRoot) {
await execa('su', [this.envSvc.userName, '-c', 'dotnet new']);
}

const ver = parse(version)!;
// command available since net core 3.1
// https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-nuget-list-source
if (ver.major > 3 || (ver.major === 3 && ver.minor >= 1)) {
// See https://github.com/NuGet/Home/issues/11607
await execa('dotnet', ['nuget', 'list', 'source']);
if (this.envSvc.isRoot) {
await execa('su', [
this.envSvc.userName,
'-c',
'dotnet nuget list source',
]);
}
}
const nuget = join(
this.envSvc.userHome,
'.config',
'NuGet',
'NuGet.Config',
);
if (await this.pathSvc.fileExists(nuget)) {
await this.pathSvc.setOwner({
file: nuget,
});
}
}

override async test(_version: string): Promise<void> {
await execa('dotnet', ['--info'], { stdio: ['inherit', 'inherit', 1] });
}
}
Loading

0 comments on commit 025ba94

Please sign in to comment.