Skip to content
Open
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
23 changes: 22 additions & 1 deletion src/installer.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,25 @@ function transformVersion (version) {
return version.replace(/(\d)[_.+-]?((RC|rc|pre|dev|beta|alpha)[_.+-]?\d*)$/, '$1~$2')
}

/**
* Recursively ensures every directory at or under `directory` has the given
* permissions. Files are left untouched so that data files aren't made
* executable.
*
* `dpkg-deb --build` rejects a control directory whose permissions are below
* 0755 ("control directory has bad permissions 756 (must be >=0755)"), which
* happens under restrictive umasks such as those used by some CI environments
* (e.g. GitHub Codespaces).
*/
async function setDirectoryPermissions (directory, mode) {
await fs.chmod(directory, mode)
for (const entry of await fs.readdir(directory, { withFileTypes: true, recursive: true })) {
if (entry.isDirectory()) {
await fs.chmod(path.join(entry.parentPath, entry.name), mode)
}
}
}

class DebianInstaller extends common.ElectronInstaller {
get contentFunctions () {
return [
Expand Down Expand Up @@ -134,6 +153,8 @@ class DebianInstaller extends common.ElectronInstaller {
async createPackage () {
this.options.logger(`Creating package at ${this.stagingDir}`)

await setDirectoryPermissions(this.stagingDir, 0o755)

const command = ['--root-owner-group', '--build', this.stagingDir]
if (this.options.compression) {
command.unshift(`-Z${this.options.compression}`)
Expand Down Expand Up @@ -283,4 +304,4 @@ export default async function createDebianInstaller (data) {
return installer.options
}

export { DebianInstaller as Installer, transformVersion }
export { DebianInstaller as Installer, setDirectoryPermissions, transformVersion }
34 changes: 33 additions & 1 deletion test/installer.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import fs from 'node:fs/promises'
import path from 'node:path'

import { spawn } from '@malept/cross-spawn-promise'
import { after, before, describe, it } from 'node:test'
import { expect } from 'chai'
import tmp from 'tmp-promise'

import installer, { transformVersion } from '../src/installer.js'
import installer, { setDirectoryPermissions, transformVersion } from '../src/installer.js'

import access from './helpers/access.js'
import describeInstaller, { cleanupOutputDir, describeInstallerWithException, tempOutputDir, testInstallerOptions } from './helpers/describe_installer.js'
Expand Down Expand Up @@ -254,6 +256,36 @@ describe('module', () => {
})
})

describe('setDirectoryPermissions', () => {
it('recursively sets directory permissions while leaving files untouched', async () => {
const root = await tmp.dir({ unsafeCleanup: true })
try {
const nestedDir = path.join(root.path, 'DEBIAN', 'nested')
await fs.mkdir(nestedDir, { recursive: true })
const file = path.join(root.path, 'DEBIAN', 'control')
await fs.writeFile(file, '')

// Simulate directories left with permissions dpkg-deb rejects (<0755).
await fs.chmod(path.join(root.path, 'DEBIAN'), 0o750)
await fs.chmod(nestedDir, 0o750)
await fs.chmod(file, 0o640)

await setDirectoryPermissions(root.path, 0o755)

for (const dir of [root.path, path.join(root.path, 'DEBIAN'), nestedDir]) {
const stats = await fs.stat(dir)
expect((stats.mode & 0o777).toString(8)).to.equal('755')
}

// Files keep their mode so data files don't become executable.
const fileStats = await fs.stat(file)
expect((fileStats.mode & 0o777).toString(8)).to.equal('640')
} finally {
await root.cleanup()
}
})
})

describeInstaller(
'with different compression type',
{
Expand Down
Loading