Skip to content

Commit bc213a5

Browse files
authored
[eas-cli] Create portable project archives (#3234)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. --> <!-- You can skip the changelog check by labeling the PR with "no changelog". --> # Why Project archives created on one platform can be extracted on another platform with incompatible metadata or mode bits. In particular, Windows read-only directories can be represented as read-only POSIX directories in the tarball, which can make worker extraction fail when child files need to be created. Using `portable: true` consistently also strips owner/group metadata and avoids making Windows a special case. Supersedes the approach in #3489 and avoids changing worker extraction as proposed in #3663. # How Create project tarballs with `portable: true` on all platforms. # Test Plan - `yarn workspace eas-cli test src/build/utils/__tests__/repository.test.ts` - `yarn workspace eas-cli typecheck` - `yarn run -T oxfmt CHANGELOG.md packages/eas-cli/src/build/utils/repository.ts packages/eas-cli/src/build/utils/__tests__/repository.test.ts`
1 parent ddd8bb7 commit bc213a5

3 files changed

Lines changed: 86 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This is the log of notable changes to EAS CLI and related packages.
1212

1313
### 🐛 Bug fixes
1414

15+
- [eas-cli] Create portable project archives on all platforms to normalize cross-platform tar metadata and permissions. ([#3234](https://github.com/expo/eas-cli/pull/3234) by [@sjchmiela](https://github.com/sjchmiela))
1516
- [eas-cli] Remove hardcoded `builderEnvironment.image` override in `eas build:resign`. ([#3661](https://github.com/expo/eas-cli/pull/3661) by [@hSATAC](https://github.com/hSATAC))
1617
- [eas-cli] Fix `eas update --json` intermittently failing with JSON parse errors during "Computing project fingerprints" by passing `silent: true` to `@expo/fingerprint` to suppress subprocess stdout pollution. ([#3659](https://github.com/expo/eas-cli/pull/3659) by [@Mookiies](https://github.com/Mookiies))
1718

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import fs from 'fs-extra';
2+
import path from 'path';
3+
import * as tar from 'tar';
4+
5+
import { Client } from '../../../vcs/vcs';
6+
import { makeProjectTarballAsync } from '../repository';
7+
8+
jest.mock('../../../ora', () => ({
9+
ora: jest.fn(() => ({
10+
fail: jest.fn(),
11+
isSpinning: false,
12+
start: jest.fn(),
13+
succeed: jest.fn(),
14+
})),
15+
}));
16+
17+
class FakeVcsClient extends Client {
18+
public async makeShallowCopyAsync(destinationPath: string): Promise<void> {
19+
await fs.mkdirp(path.join(destinationPath, 'bin'));
20+
await fs.mkdirp(path.join(destinationPath, 'read-only-dir'));
21+
22+
await fs.writeFile(path.join(destinationPath, 'bin', 'postcheckout.sh'), '#!/bin/sh\necho hi\n');
23+
await fs.writeFile(path.join(destinationPath, 'regular-file.txt'), 'regular file\n');
24+
await fs.writeFile(path.join(destinationPath, 'read-only-dir', 'child.txt'), 'child file\n');
25+
26+
await fs.chmod(path.join(destinationPath, 'bin', 'postcheckout.sh'), 0o755);
27+
await fs.chmod(path.join(destinationPath, 'regular-file.txt'), 0o644);
28+
await fs.chmod(path.join(destinationPath, 'read-only-dir'), 0o555);
29+
}
30+
31+
public async getRootPathAsync(): Promise<string> {
32+
return process.cwd();
33+
}
34+
35+
public canGetLastCommitMessage(): boolean {
36+
return false;
37+
}
38+
}
39+
40+
describe(makeProjectTarballAsync, () => {
41+
it('creates portable project archives while preserving executable files', async () => {
42+
const removeAsync = fs.remove.bind(fs);
43+
const removeSpy = jest.spyOn(fs, 'remove').mockImplementation(async targetPath => {
44+
await fs.chmod(path.join(targetPath.toString(), 'read-only-dir'), 0o755).catch(() => {});
45+
await removeAsync(targetPath);
46+
});
47+
const projectTarball = await makeProjectTarballAsync(new FakeVcsClient());
48+
const entries = new Map<string, tar.ReadEntry>();
49+
50+
try {
51+
await tar.list({
52+
file: projectTarball.path,
53+
onentry: entry => {
54+
entries.set(entry.path, entry);
55+
},
56+
});
57+
58+
expect(entries.get('project/bin/postcheckout.sh')?.mode).toBe(0o755);
59+
expect(entries.get('project/regular-file.txt')?.mode).toBe(0o644);
60+
expect(entries.get('project/read-only-dir/')?.mode).toBe(0o755);
61+
expect(entries.has('project/read-only-dir/child.txt')).toBe(true);
62+
63+
for (const entry of entries.values()) {
64+
expect(entry.path.startsWith('project/')).toBe(true);
65+
expect(entry.uid).toBeUndefined();
66+
expect(entry.gid).toBeUndefined();
67+
expect(entry.uname).toBe('');
68+
expect(entry.gname).toBe('');
69+
}
70+
} finally {
71+
await fs.remove(projectTarball.path);
72+
removeSpy.mockRestore();
73+
}
74+
});
75+
});

packages/eas-cli/src/build/utils/repository.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,16 @@ export async function makeProjectTarballAsync(vcsClient: Client): Promise<LocalF
143143

144144
try {
145145
await vcsClient.makeShallowCopyAsync(shallowClonePath);
146-
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
147-
'.',
148-
]);
146+
await tar.create(
147+
{
148+
cwd: shallowClonePath,
149+
file: tarPath,
150+
prefix: 'project',
151+
gzip: true,
152+
portable: true,
153+
},
154+
['.']
155+
);
149156
} catch (err) {
150157
clearTimeout(timer);
151158
if (spinner.isSpinning) {

0 commit comments

Comments
 (0)