Skip to content

Commit 6f8c086

Browse files
authored
Windows CI (#21)
* Update ci.yml * Implements ls-remote without spawning the git binary * Implements enable/disable for Windows * Triggers CI * Mocks HTTP calls * Fixes the shell on Windows * Increases the timeout, thanks Windows
1 parent 25e91d8 commit 6f8c086

36 files changed

+295
-97
lines changed

.github/workflows/ci.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
platform:
1717
- ubuntu-latest
1818
- macos-latest
19+
- windows-latest
1920

2021
name: '${{matrix.platform}} w/ Node.js ${{matrix.node}}.x'
2122
runs-on: ${{matrix.platform}}
@@ -31,4 +32,5 @@ jobs:
3132
- run: yarn install --immutable
3233
- run: yarn build # We need the stubs to run the tests
3334
- run: yarn eslint
34-
- run: yarn jest
35+
- run: NOCK_ENV=replay yarn jest
36+
shell: bash

.pnp.js

+33
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Binary file not shown.
Binary file not shown.
Binary file not shown.

jest.config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
module.exports = {
2-
testTimeout: 30000,
2+
setupFilesAfterEnv: [`./tests/setupTests.js`],
3+
testTimeout: 120000,
34
};

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"eslint": "^7.10.0",
3333
"eslint-plugin-arca": "^0.9.5",
3434
"jest": "^25.1.0",
35+
"nock": "^13.0.4",
3536
"semver": "^7.1.3",
3637
"supports-color": "^7.1.0",
3738
"tar": "^6.0.1",

sources/commands/Disable.ts

+26-17
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,6 @@ export class DisableCommand extends Command<Context> {
4242
if (typeof installDirectory === `undefined`)
4343
installDirectory = path.dirname(await which(`corepack`));
4444

45-
if (process.platform === `win32`) {
46-
return this.executeWin32(installDirectory);
47-
} else {
48-
return this.executePosix(installDirectory);
49-
}
50-
}
51-
52-
async executePosix(installDirectory: string) {
5345
const names = this.names.length === 0
5446
? SupportedPackageManagerSet
5547
: this.names;
@@ -59,19 +51,36 @@ export class DisableCommand extends Command<Context> {
5951
throw new UsageError(`Invalid package manager name '${name}'`);
6052

6153
for (const binName of this.context.engine.getBinariesFor(name)) {
62-
const file = path.join(installDirectory, binName);
63-
try {
64-
await fs.promises.unlink(file);
65-
} catch (err) {
66-
if (err.code !== `ENOENT`) {
67-
throw err;
68-
}
54+
if (process.platform === `win32`) {
55+
await this.removeWin32Link(installDirectory, binName);
56+
} else {
57+
await this.removePosixLink(installDirectory, binName);
6958
}
7059
}
7160
}
7261
}
7362

74-
async executeWin32(installDirectory: string) {
75-
throw new UsageError(`This command isn't available on Windows at this time`);
63+
async removePosixLink(installDirectory: string, binName: string) {
64+
const file = path.join(installDirectory, binName);
65+
try {
66+
await fs.promises.unlink(file);
67+
} catch (err) {
68+
if (err.code !== `ENOENT`) {
69+
throw err;
70+
}
71+
}
72+
}
73+
74+
async removeWin32Link(installDirectory: string, binName: string) {
75+
for (const ext of [``, `.ps1`, `.cmd`]) {
76+
const file = path.join(installDirectory, `${binName}${ext}`);
77+
try {
78+
await fs.promises.unlink(file);
79+
} catch (err) {
80+
if (err.code !== `ENOENT`) {
81+
throw err;
82+
}
83+
}
84+
}
7685
}
7786
}

sources/commands/Enable.ts

+25-21
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import cmdShim from '@zkochan/cmd-shim';
12
import {Command, UsageError} from 'clipanion';
23
import fs from 'fs';
34
import path from 'path';
@@ -42,14 +43,6 @@ export class EnableCommand extends Command<Context> {
4243
if (typeof installDirectory === `undefined`)
4344
installDirectory = path.dirname(await which(`corepack`));
4445

45-
if (process.platform === `win32`) {
46-
return this.executeWin32(installDirectory);
47-
} else {
48-
return this.executePosix(installDirectory);
49-
}
50-
}
51-
52-
async executePosix(installDirectory: string) {
5346
// We use `eval` so that Webpack doesn't statically transform it.
5447
const manifestPath = eval(`require`).resolve(`corepack/package.json`);
5548

@@ -66,24 +59,35 @@ export class EnableCommand extends Command<Context> {
6659
throw new UsageError(`Invalid package manager name '${name}'`);
6760

6861
for (const binName of this.context.engine.getBinariesFor(name)) {
69-
const file = path.join(installDirectory, binName);
70-
const symlink = path.relative(installDirectory, path.join(stubFolder, binName));
71-
72-
if (fs.existsSync(file)) {
73-
const currentSymlink = await fs.promises.readlink(file);
74-
if (currentSymlink !== symlink) {
75-
await fs.promises.unlink(file);
76-
} else {
77-
return;
78-
}
62+
if (process.platform === `win32`) {
63+
await this.generateWin32Link(installDirectory, stubFolder, binName);
64+
} else {
65+
await this.generatePosixLink(installDirectory, stubFolder, binName);
7966
}
67+
}
68+
}
69+
}
8070

81-
await fs.promises.symlink(symlink, file);
71+
async generatePosixLink(installDirectory: string, stubFolder: string, binName: string) {
72+
const file = path.join(installDirectory, binName);
73+
const symlink = path.relative(installDirectory, path.join(stubFolder, binName));
74+
75+
if (fs.existsSync(file)) {
76+
const currentSymlink = await fs.promises.readlink(file);
77+
if (currentSymlink !== symlink) {
78+
await fs.promises.unlink(file);
79+
} else {
80+
return;
8281
}
8382
}
83+
84+
await fs.promises.symlink(symlink, file);
8485
}
8586

86-
async executeWin32(target: string) {
87-
throw new UsageError(`This command isn't available on Windows at this time`);
87+
async generateWin32Link(installDirectory: string, stubFolder: string, binName: string) {
88+
const file = path.join(installDirectory, binName);
89+
await cmdShim(path.join(stubFolder, binName), file, {
90+
createCmdFile: true,
91+
});
8892
}
8993
}

sources/gitUtils.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as httpUtils from './httpUtils';
2+
3+
export async function lsRemote(url: string) {
4+
const data = await httpUtils.fetchAsBuffer(`${url}/info/refs?service=git-upload-pack`);
5+
const refs = new Set<string>();
6+
7+
let t = 0;
8+
while (t < data.length) {
9+
const lenMarker = data.slice(t, t + 4).toString();
10+
const len = parseInt(lenMarker, 16);
11+
12+
// flush-pkt
13+
if (len === 0) {
14+
t += 4;
15+
continue;
16+
}
17+
18+
// Substract 1 to remove the trailing newline
19+
const line = data.slice(t + 4, t + len - 1).toString();
20+
t += len;
21+
22+
if (line.startsWith(`#`))
23+
continue;
24+
25+
let nameEnd = line.indexOf(`\0`);
26+
if (nameEnd === -1)
27+
nameEnd = line.length;
28+
29+
const nameStart = line.indexOf(` `);
30+
if (nameStart === -1 || nameStart >= nameEnd)
31+
continue;
32+
33+
const name = line.slice(nameStart + 1, nameEnd);
34+
if (name === `capabilities^{}`)
35+
return new Set<string>();
36+
37+
refs.add(name);
38+
}
39+
40+
return refs;
41+
}
42+
43+
// smart_reply = PKT-LINE("# service=$servicename" LF)
44+
// ref_list
45+
// "0000"
46+
//
47+
// ref_list = empty_list / non_empty_list
48+
//
49+
// empty_list = PKT-LINE(zero-id SP "capabilities^{}" NUL cap-list LF)
50+
//
51+
// non_empty_list = PKT-LINE(obj-id SP name NUL cap_list LF)
52+
// *ref_record
53+
//
54+
// cap-list = capability *(SP capability)
55+
// capability = 1*(LC_ALPHA / DIGIT / "-" / "_")
56+
// LC_ALPHA = %x61-7A
57+
58+
// ref_record = any_ref / peeled_ref
59+
// any_ref = PKT-LINE(obj-id SP name LF)
60+
// peeled_ref = PKT-LINE(obj-id SP name LF)

sources/pmmUtils.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,16 @@ import fs from 'fs';
33
import path from 'path';
44
import semver from 'semver';
55
import tar from 'tar';
6-
import {promisify} from 'util';
76

87
import * as debugUtils from './debugUtils';
98
import * as folderUtils from './folderUtils';
109
import * as fsUtils from './fsUtils';
10+
import * as gitUtils from './gitUtils';
1111
import * as httpUtils from './httpUtils';
1212
import {Context} from './main';
1313
import {TagSpec, Descriptor, Locator, PackageManagerSpec} from './types';
1414

15-
const execFileP = promisify(execFile);
16-
17-
const NL_REGEXP = /\n/;
18-
const REFS_TAGS_REGEXP = /^[a-f0-9]+\trefs\/tags\/(.*)\^\{\}$/;
15+
const REFS_TAGS_REGEXP = /^refs\/tags\/(.*)(?:\^\{\})?$/;
1916

2017
export async function fetchAvailableVersions(spec: TagSpec) {
2118
switch (spec.type) {
@@ -25,14 +22,13 @@ export async function fetchAvailableVersions(spec: TagSpec) {
2522
} break;
2623

2724
case `git`: {
28-
const {stdout} = await execFileP(`git`, [`ls-remote`, `--tags`, spec.repository]);
29-
const lines = stdout.split(NL_REGEXP);
25+
const refs = await gitUtils.lsRemote(spec.repository);
3026

3127
const regexp = new RegExp(`^${spec.pattern.replace(`{}`, `(.*)`)}$`);
3228

3329
const results = [];
34-
for (const line of lines) {
35-
const lv1 = line.match(REFS_TAGS_REGEXP);
30+
for (const ref of refs) {
31+
const lv1 = ref.match(REFS_TAGS_REGEXP);
3632
if (!lv1)
3733
continue;
3834

0 commit comments

Comments
 (0)