From 6af9fb046fb236f47e5f153e1e900c2b829e00c8 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 14 Jul 2021 11:53:03 -0300 Subject: [PATCH] feat(tooling): flatpak - introduce abstractions to make Flatpak integration easier (#555 #558) Co-Authored-By: nullrequest <30698906+advaithm@users.noreply.github.com> --- app/src/lib/editors/launch.ts | 4 +- app/src/lib/editors/linux.ts | 3 +- app/src/lib/helpers/linux.ts | 98 +++++++++++++++++++++++++++++ app/src/lib/shells/linux.ts | 4 +- app/src/lib/shells/shared.ts | 2 +- app/test/unit/helpers/linux-test.ts | 47 ++++++++++++++ 6 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 app/src/lib/helpers/linux.ts create mode 100644 app/test/unit/helpers/linux-test.ts diff --git a/app/src/lib/editors/launch.ts b/app/src/lib/editors/launch.ts index ed00ff396ff..8e83b9f8c3d 100644 --- a/app/src/lib/editors/launch.ts +++ b/app/src/lib/editors/launch.ts @@ -1,5 +1,5 @@ import { spawn, SpawnOptions } from 'child_process' -import { pathExists } from '../../ui/lib/path-exists' +import { pathExists, spawnEditor } from '../helpers/linux' import { ExternalEditorError, FoundEditor } from './shared' import { expandTargetPathArgument, @@ -42,6 +42,8 @@ export async function launchExternalEditor( // In macOS we can use `open`, which will open the right executable file // for us, we only need the path to the editor .app folder. spawn('open', ['-a', editorPath, fullPath], opts) + } else if (__LINUX__) { + spawnEditor(editorPath, fullPath, opts) } else { spawn(editorPath, [fullPath], opts) } diff --git a/app/src/lib/editors/linux.ts b/app/src/lib/editors/linux.ts index 5d9c527622d..83e41dc3b8f 100644 --- a/app/src/lib/editors/linux.ts +++ b/app/src/lib/editors/linux.ts @@ -1,4 +1,5 @@ -import { pathExists } from '../../ui/lib/path-exists' +import { pathExists } from '../helpers/linux' + import { IFoundEditor } from './found-editor' /** Represents an external editor on Linux */ diff --git a/app/src/lib/helpers/linux.ts b/app/src/lib/helpers/linux.ts new file mode 100644 index 00000000000..aac3701dd95 --- /dev/null +++ b/app/src/lib/helpers/linux.ts @@ -0,0 +1,98 @@ +import { join } from 'path' +import { pathExists as pathExistsInternal } from 'fs-extra' +import { + ChildProcess, + spawn as nodeSpawn, + SpawnOptionsWithoutStdio, + SpawnOptions, +} from 'child_process' + +export function isFlatpakBuild() { + return __LINUX__ && process.env.FLATPAK_HOST === '1' +} + +/** + * Convert an executable path to be relative to the flatpak host + * + * @param path a path to an executable relative to the root of the filesystem + */ +export function convertToFlatpakPath(path: string) { + if (!__LINUX__) { + return path + } + + if (path.startsWith('/opt/')) { + return path + } + + return join('/var/run/host', path) +} + +export function formatWorkingDirectoryForFlatpak(path: string): string { + return path.replace(/(\s)/, ' ') +} + +/** + * Checks the file path on disk exists before attempting to launch a specific shell + * + * @param path + * + * @returns `true` if the path can be resolved, or `false` otherwise + */ +export async function pathExists(path: string): Promise { + if (isFlatpakBuild()) { + path = convertToFlatpakPath(path) + } + + try { + return await pathExistsInternal(path) + } catch { + return false + } +} + +/** + * Spawn a particular shell in a way that works for Flatpak-based usage + * + * @param path path to shell, relative to the root of the filesystem + * @param args arguments to provide to the shell + * @param options additional options to provide to spawn function + * + * @returns a child process to observe and monitor + */ +export function spawn( + path: string, + args: ReadonlyArray, + options?: SpawnOptionsWithoutStdio +): ChildProcess { + if (isFlatpakBuild()) { + return nodeSpawn('flatpak-spawn', ['--host', path, ...args], options) + } + + return nodeSpawn(path, args, options) +} + +/** + * Spawn a given editor in a way that works for Flatpak-based usage + * + * @param path path to editor, relative to the root of the filesystem + * @param workingDirectory working directory to open initially in editor + * @param options additional options to provide to spawn function + */ +export function spawnEditor( + path: string, + workingDirectory: string, + options: SpawnOptions +): ChildProcess { + if (isFlatpakBuild()) { + const EscapedworkingDirectory = + formatWorkingDirectoryForFlatpak(workingDirectory) + return nodeSpawn( + 'flatpak-spawn', + ['--host', path, EscapedworkingDirectory], + options + ) + } else { + return nodeSpawn(path, [workingDirectory], options) + } +} diff --git a/app/src/lib/shells/linux.ts b/app/src/lib/shells/linux.ts index afb8087cc31..5800dd1f2f5 100644 --- a/app/src/lib/shells/linux.ts +++ b/app/src/lib/shells/linux.ts @@ -1,7 +1,6 @@ -import { spawn, ChildProcess } from 'child_process' +import { ChildProcess } from 'child_process' import { assertNever } from '../fatal-error' import { parseEnumValue } from '../enum' -import { pathExists } from '../../ui/lib/path-exists' import { FoundShell } from './shared' import { expandTargetPathArgument, @@ -9,6 +8,7 @@ import { parseCustomIntegrationArguments, spawnCustomIntegration, } from '../custom-integration' +import { pathExists, spawn } from '../helpers/linux' export enum Shell { Gnome = 'GNOME Terminal', diff --git a/app/src/lib/shells/shared.ts b/app/src/lib/shells/shared.ts index 25e52bfd3ea..211a8265793 100644 --- a/app/src/lib/shells/shared.ts +++ b/app/src/lib/shells/shared.ts @@ -3,8 +3,8 @@ import { ChildProcess } from 'child_process' import * as Darwin from './darwin' import * as Win32 from './win32' import * as Linux from './linux' +import { pathExists } from '../helpers/linux' import { ShellError } from './error' -import { pathExists } from '../../ui/lib/path-exists' import { ICustomIntegration } from '../custom-integration' export type Shell = Darwin.Shell | Win32.Shell | Linux.Shell diff --git a/app/test/unit/helpers/linux-test.ts b/app/test/unit/helpers/linux-test.ts new file mode 100644 index 00000000000..52873f4e9d0 --- /dev/null +++ b/app/test/unit/helpers/linux-test.ts @@ -0,0 +1,47 @@ +import { + convertToFlatpakPath, + formatWorkingDirectoryForFlatpak, +} from '../../../src/lib/helpers/linux' + +describe('convertToFlatpakPath()', () => { + if (__LINUX__) { + it('converts /usr paths', () => { + const path = '/usr/bin/subl' + const expectedPath = '/var/run/host/usr/bin/subl' + expect(convertToFlatpakPath(path)).toEqual(expectedPath) + }) + + it('preserves /opt paths', () => { + const path = '/opt/slickedit-pro2018/bin/vs' + expect(convertToFlatpakPath(path)).toEqual(path) + }) + } + + if (__WIN32__) { + it('returns same path', () => { + const path = 'C:\\Windows\\System32\\Notepad.exe' + expect(convertToFlatpakPath(path)).toEqual(path) + }) + } + + if (__DARWIN__) { + it('returns same path', () => { + const path = '/usr/local/bin/code' + expect(convertToFlatpakPath(path)).toEqual(path) + }) + } +}) + +describe('formatWorkingDirectoryForFlatpak()', () => { + if (__LINUX__) { + it('escapes string', () => { + const path = '/home/test/path with space' + const expectedPath = '/home/test/path with space' + expect(formatWorkingDirectoryForFlatpak(path)).toEqual(expectedPath) + }) + it('returns same path', () => { + const path = '/home/test/path_wthout_spaces' + expect(formatWorkingDirectoryForFlatpak(path)).toEqual(path) + }) + } +})