Skip to content

Commit fa6e95c

Browse files
claudeianho
andcommitted
fix: support tmpdir: false when the out dir is inside the project dir
Node's fs.cp rejects copying a directory into a subdirectory of itself before the copy filter runs, so packaging with tmpdir: false and an out dir inside the project dir (e.g. Electron Forge's default ./out) failed with ERR_FS_CP_EINVAL. When the staging path is inside the app dir, copy each top-level entry individually instead, still applying the same filter so ignored paths (including the out dir itself) are skipped. Fixes #1679 Co-authored-by: ianho <anho.zhang@gmail.com>
1 parent 234c514 commit fa6e95c

2 files changed

Lines changed: 55 additions & 3 deletions

File tree

src/platform.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,36 @@ export class App {
183183
async copyTemplate() {
184184
await runHooks(this.opts.beforeCopy, this.hookArgsWithOriginalResourcesAppDir);
185185

186-
await fs.promises.cp(this.opts.dir, this.originalResourcesAppDir, {
186+
const filter = userPathFilter(this.opts)!;
187+
const copyOpts = {
187188
recursive: true,
188-
filter: userPathFilter(this.opts),
189+
filter,
189190
dereference: typeof this.opts.derefSymlinks === 'boolean' ? this.opts.derefSymlinks : true,
190-
});
191+
};
192+
193+
const src = path.resolve(this.opts.dir);
194+
const dest = path.resolve(this.originalResourcesAppDir);
195+
const relativeDest = path.relative(src, dest);
196+
if (
197+
relativeDest &&
198+
relativeDest !== '..' &&
199+
!relativeDest.startsWith(`..${path.sep}`) &&
200+
!path.isAbsolute(relativeDest)
201+
) {
202+
// The destination is inside the app dir (e.g. tmpdir: false with the out dir
203+
// inside the project dir). fs.cp rejects dest-inside-src before the filter runs,
204+
// so copy each top-level entry individually instead.
205+
await fs.promises.mkdir(dest, { recursive: true });
206+
for (const entry of await fs.promises.readdir(src)) {
207+
const srcEntry = path.join(src, entry);
208+
const destEntry = path.join(dest, entry);
209+
if (await filter(srcEntry, destEntry)) {
210+
await fs.promises.cp(srcEntry, destEntry, copyOpts);
211+
}
212+
}
213+
} else {
214+
await fs.promises.cp(this.opts.dir, this.originalResourcesAppDir, copyOpts);
215+
}
191216
await runHooks(this.opts.afterCopy, this.hookArgsWithOriginalResourcesAppDir);
192217
if (this.opts.prune) {
193218
await runHooks(this.opts.afterPrune, this.hookArgsWithOriginalResourcesAppDir);

test/packager.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,33 @@ describe('packager', () => {
179179
expect(paths[0]).toBeDirectory();
180180
});
181181

182+
// https://github.com/electron/packager/issues/1679
183+
it('can package with tmpdir disabled and the out dir inside the project dir', async ({
184+
baseOpts,
185+
}) => {
186+
const projectDir = path.join(baseOpts.tmpdir as string, 'project');
187+
await fs.promises.cp(baseOpts.dir, projectDir, { recursive: true });
188+
189+
const opts = {
190+
...baseOpts,
191+
dir: projectDir,
192+
out: path.join(projectDir, 'out'),
193+
tmpdir: false,
194+
asar: false,
195+
platform: 'linux',
196+
arch: 'x64',
197+
} as const;
198+
199+
const paths = await packager(opts);
200+
expect(paths).toHaveLength(1);
201+
expect(paths[0]).toBeDirectory();
202+
203+
const appDir = path.join(paths[0], 'resources', 'app');
204+
expect(path.join(appDir, 'main.js')).toBeFile();
205+
// The out dir must not be copied into the packaged app
206+
expect(fs.existsSync(path.join(appDir, 'out'))).toBe(false);
207+
});
208+
182209
it('preserves symlinks with derefSymlinks disabled', async ({ baseOpts }) => {
183210
const opts = {
184211
...baseOpts,

0 commit comments

Comments
 (0)