Skip to content

Commit 41369c9

Browse files
committed
feat(linux): register debug builds with the desktop
ns run/debug now installs the staged <app-id>.desktop and icons into $XDG_DATA_HOME, so the shell matches the window to the app and shows its name and icon (dock, "not responding" dialog). Entries it didn't write, such as one from install.sh, are left alone; release stages are skipped.
1 parent c8236f6 commit 41369c9

3 files changed

Lines changed: 171 additions & 0 deletions

File tree

‎lib/common/mobile/linux/linux-application-manager.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ChildProcess, spawn } from "child_process";
44
import { ApplicationManagerBase } from "../application-manager-base";
55
import { IHooksService, IDictionary } from "../../declarations";
66
import { IBuildData } from "../../../definitions/build";
7+
import { installDebugDesktopEntry } from "../../../services/linux-desktop-entry";
78

89
/** Port of the runtime's DevTools inspector for `ns debug linux`. */
910
export const LINUX_INSPECTOR_PORT = 9229;
@@ -53,6 +54,13 @@ export class LinuxApplicationManager extends ApplicationManagerBase {
5354
this._executables[path.basename(packageFilePath)] = packageFilePath;
5455
if (appIdentifier) {
5556
this._executables[appIdentifier] = packageFilePath;
57+
try {
58+
installDebugDesktopEntry(path.dirname(packageFilePath), appIdentifier);
59+
} catch (e) {
60+
this.$logger.warn(
61+
`Couldn't register ${appIdentifier} with the desktop: ${e?.message ?? e}`,
62+
);
63+
}
5664
}
5765
}
5866

‎lib/services/linux-desktop-entry.ts‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
* metainfo and install.sh, filled from App_Resources/Linux templates.
44
*/
55

6+
import * as fs from "fs";
7+
import * as os from "os";
8+
import * as path from "path";
9+
610
/** Reverse-DNS style id; it names files (`<id>.desktop`) and the install folder. */
711
const APP_ID = /^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/;
812

@@ -63,6 +67,59 @@ export function shellQuote(value: string): string {
6367
return `'${value.replace(/'/g, `'\\''`)}'`;
6468
}
6569

70+
/** Marks entries written by debug runs. */
71+
export const DEBUG_ENTRY_KEY = "X-NativeScript-Debug";
72+
73+
export function xdgDataHome(): string {
74+
const value = process.env.XDG_DATA_HOME;
75+
return value && path.isAbsolute(value)
76+
? value
77+
: path.join(os.homedir(), ".local", "share");
78+
}
79+
80+
/** Installs a debug build's desktop entry and icons; never replaces an entry it didn't write. */
81+
export function installDebugDesktopEntry(
82+
stage: string,
83+
appId: string,
84+
dataHome: string = xdgDataHome(),
85+
): boolean {
86+
assertLinuxAppId(appId);
87+
const source = path.join(stage, `${appId}.desktop`);
88+
if (!fs.existsSync(source)) {
89+
return false;
90+
}
91+
const entry = fs.readFileSync(source, "utf8");
92+
if (entry.includes("__EXEC__")) {
93+
return false;
94+
}
95+
const applications = path.join(dataHome, "applications");
96+
const destination = path.join(applications, `${appId}.desktop`);
97+
if (
98+
fs.existsSync(destination) &&
99+
!fs
100+
.readFileSync(destination, "utf8")
101+
.split(/\r?\n/)
102+
.includes(`${DEBUG_ENTRY_KEY}=true`)
103+
) {
104+
return false;
105+
}
106+
// before any action groups
107+
const group = /^\[Desktop Entry\][^\S\n]*$/m;
108+
const marked = group.test(entry)
109+
? entry.replace(group, (header) => `${header}\n${DEBUG_ENTRY_KEY}=true`)
110+
: `[Desktop Entry]\n${DEBUG_ENTRY_KEY}=true\n${entry}`;
111+
fs.mkdirSync(applications, { recursive: true });
112+
const temp = `${destination}.${process.pid}.tmp`;
113+
fs.writeFileSync(temp, marked);
114+
fs.renameSync(temp, destination);
115+
116+
const icons = path.join(stage, "icons");
117+
if (fs.existsSync(icons)) {
118+
fs.cpSync(icons, path.join(dataHome, "icons"), { recursive: true });
119+
}
120+
return true;
121+
}
122+
66123
/**
67124
* install.sh of release packages: copies the app into $XDG_DATA_HOME/<id> and
68125
* fills the desktop entry's `__EXEC__` with the quoted executable path.

‎test/services/linux-desktop-entry.ts‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import { execFileSync } from "child_process";
55
import { assert } from "chai";
66
import {
77
assertLinuxAppId,
8+
DEBUG_ENTRY_KEY,
89
desktopExecArg,
910
desktopString,
1011
fillTemplate,
12+
installDebugDesktopEntry,
1113
installScript,
1214
xmlEscape,
1315
} from "../../lib/services/linux-desktop-entry";
@@ -75,4 +77,108 @@ describe("linuxDesktopEntry", () => {
7577
fs.rmSync(root, { recursive: true, force: true });
7678
}
7779
});
80+
81+
describe("installDebugDesktopEntry", () => {
82+
let root: string;
83+
let stage: string;
84+
let data: string;
85+
const installed = () =>
86+
fs.readFileSync(
87+
path.join(data, "applications", "org.x.app.desktop"),
88+
"utf8",
89+
);
90+
91+
beforeEach(() => {
92+
root = fs.mkdtempSync(path.join(os.tmpdir(), "ns-linux-debug-entry-"));
93+
stage = path.join(root, "stage");
94+
data = path.join(root, "data");
95+
fs.mkdirSync(path.join(stage, "icons", "hicolor", "512x512", "apps"), {
96+
recursive: true,
97+
});
98+
fs.writeFileSync(
99+
path.join(
100+
stage,
101+
"icons",
102+
"hicolor",
103+
"512x512",
104+
"apps",
105+
"org.x.app.png",
106+
),
107+
"png",
108+
);
109+
});
110+
111+
afterEach(() => {
112+
fs.rmSync(root, { recursive: true, force: true });
113+
});
114+
115+
it("installs the entry, marked as a debug entry, and its icons", () => {
116+
fs.writeFileSync(
117+
path.join(stage, "org.x.app.desktop"),
118+
'[Desktop Entry]\nName=App\nExec="/s/app"\n\n[Desktop Action new]\nName=New\n',
119+
);
120+
assert.isTrue(installDebugDesktopEntry(stage, "org.x.app", data));
121+
assert.equal(
122+
installed(),
123+
`[Desktop Entry]\n${DEBUG_ENTRY_KEY}=true\nName=App\nExec="/s/app"\n\n[Desktop Action new]\nName=New\n`,
124+
);
125+
assert.isTrue(
126+
fs.existsSync(
127+
path.join(
128+
data,
129+
"icons",
130+
"hicolor",
131+
"512x512",
132+
"apps",
133+
"org.x.app.png",
134+
),
135+
),
136+
);
137+
});
138+
139+
it("replaces its own entry on later runs", () => {
140+
const entry = path.join(stage, "org.x.app.desktop");
141+
fs.writeFileSync(entry, "[Desktop Entry]\nName=Old\n");
142+
installDebugDesktopEntry(stage, "org.x.app", data);
143+
fs.writeFileSync(entry, "[Desktop Entry]\nName=New\n");
144+
assert.isTrue(installDebugDesktopEntry(stage, "org.x.app", data));
145+
assert.equal(
146+
installed(),
147+
`[Desktop Entry]\n${DEBUG_ENTRY_KEY}=true\nName=New\n`,
148+
);
149+
});
150+
151+
it("leaves an installed release entry alone", () => {
152+
fs.mkdirSync(path.join(data, "applications"), { recursive: true });
153+
const release =
154+
'[Desktop Entry]\nName=App\nExec="/home/u/.local/share/org.x.app/app"\n';
155+
fs.writeFileSync(
156+
path.join(data, "applications", "org.x.app.desktop"),
157+
release,
158+
);
159+
fs.writeFileSync(
160+
path.join(stage, "org.x.app.desktop"),
161+
'[Desktop Entry]\nName=App\nExec="/s/app"\n',
162+
);
163+
assert.isFalse(installDebugDesktopEntry(stage, "org.x.app", data));
164+
assert.equal(installed(), release);
165+
});
166+
167+
it("skips release stages and stages without an entry", () => {
168+
assert.isFalse(installDebugDesktopEntry(stage, "org.x.app", data));
169+
fs.writeFileSync(
170+
path.join(stage, "org.x.app.desktop"),
171+
"[Desktop Entry]\nExec=__EXEC__\n",
172+
);
173+
assert.isFalse(installDebugDesktopEntry(stage, "org.x.app", data));
174+
assert.isFalse(fs.existsSync(path.join(data, "applications")));
175+
});
176+
177+
it("rejects app ids that aren't file names", () => {
178+
assert.throws(
179+
() => installDebugDesktopEntry(stage, "../evil", data),
180+
/Invalid Linux app id/,
181+
);
182+
});
183+
});
78184
});

0 commit comments

Comments
 (0)