Skip to content
Draft
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
4 changes: 4 additions & 0 deletions lib/common/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ injector.require(
"iOSSimulatorDiscovery",
"./mobile/mobile-core/ios-simulator-discovery",
);
injector.require(
"iOSMacDeviceDiscovery",
"./mobile/mobile-core/ios-mac-device-discovery",
);
injector.require(
"androidDeviceDiscovery",
"./mobile/mobile-core/android-device-discovery",
Expand Down
175 changes: 175 additions & 0 deletions lib/common/mobile/ios/mac/ios-mac-application-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import * as path from "path";
import { ApplicationManagerBase } from "../../application-manager-base";
import { hook, sleep } from "../../../helpers";
import { cache } from "../../../decorators";
import { IOS_LOG_PREDICATE } from "../../../constants";
import {
IChildProcess,
IDictionary,
IFileSystem,
IHooksService,
IPlistParser,
} from "../../../declarations";
import { IOptions } from "../../../../declarations";

interface IIOSMacApplicationHost extends Mobile.IiOSDevice {
readonly wrapperPath: string;
readonly applicationBundlePath: string;
readonly executableName: string;
}

export class IOSMacApplicationManager extends ApplicationManagerBase {
constructor(
private device: IIOSMacApplicationHost,
private $childProcess: IChildProcess,
private $fs: IFileSystem,
private $options: IOptions,
private $plistParser: IPlistParser,
protected $deviceLogProvider: Mobile.IDeviceLogProvider,
$logger: ILogger,
$hooksService: IHooksService,
) {
super($logger, $hooksService, $deviceLogProvider);
}

public async getInstalledApplications(): Promise<string[]> {
const infoPlistPath = path.join(
this.device.applicationBundlePath,
"Info.plist",
);
if (!this.$fs.exists(infoPlistPath)) {
return [];
}

const infoPlist = await this.$plistParser.parseFile(infoPlistPath);
return infoPlist?.CFBundleIdentifier ? [infoPlist.CFBundleIdentifier] : [];
}

@hook("install")
public async installApplication(packageFilePath: string): Promise<void> {
const wrapperPath = this.device.wrapperPath;
this.$fs.deleteDirectory(wrapperPath);
this.$fs.ensureDirectoryExists(path.join(wrapperPath, "Wrapper"));
// ditto keeps the bundle's symlinks and extended attributes, which its
// code signature covers.
await this.$childProcess.spawnFromEvent(
"ditto",
[packageFilePath, this.device.applicationBundlePath],
"close",
);
this.$fs.symlink(
path.join("Wrapper", path.basename(this.device.applicationBundlePath)),
path.join(wrapperPath, "WrappedBundle"),
);
}

public async uninstallApplication(appIdentifier: string): Promise<void> {
await this.stopApplication({
appId: appIdentifier,
projectName: null,
projectDir: null,
});
this.$fs.deleteDirectory(this.device.wrapperPath);
}

public async startApplication(
appData: Mobile.IStartApplicationData,
): Promise<void> {
await this.setDeviceLogData(appData);

const args = ["-n", this.device.wrapperPath];
if (appData.waitForDebugger) {
args.push("--args", "--nativescript-debug-brk");
}
await this.$childProcess.spawnFromEvent("open", args, "close");

const [pid] = await this.getRunningPids();
if (pid) {
this.$deviceLogProvider.setApplicationPidForDevice(
this.device.deviceInfo.identifier,
pid,
);
}
}

public async stopApplication(
appData: Mobile.IApplicationData,
): Promise<void> {
await this.device.destroyDebugSocket(appData.appId);

for (const signal of ["TERM", "KILL"]) {
const pids = await this.getRunningPids();
if (!pids.length) {
return;
}

await this.$childProcess
.spawnFromEvent("kill", [`-${signal}`, ...pids], "close")
.catch((err) => this.$logger.trace(err));

// `open -n` while the old instance is still exiting starts a second copy.
for (let attempt = 0; attempt < 40; attempt++) {
if (!(await this.getRunningPids()).length) {
return;
}
await sleep(50);
}
}
}

public async getDebuggableApps(): Promise<
Mobile.IDeviceApplicationInformation[]
> {
return [];
}

public async getDebuggableAppViews(
appIdentifiers: string[],
): Promise<IDictionary<Mobile.IDebugWebViewInfo[]>> {
return null;
}

/**
* macOS runs the app from a copy of the wrapper under /private/var/folders,
* so match the bundle layout rather than the path the CLI installed to.
*/
private async getRunningPids(): Promise<string[]> {
const name = this.device.executableName.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&",
);
try {
const result = await this.$childProcess.spawnFromEvent(
"pgrep",
["-f", `/Wrapper/${name}\\.app/${name}( |$)`],
"close",
);
return result.stdout.split(/\s+/).filter(Boolean);
} catch (err) {
// pgrep exits with 1 when nothing matches.
return [];
}
}

private async setDeviceLogData(
appData: Mobile.IApplicationData,
): Promise<void> {
this.$deviceLogProvider.setProjectNameForDevice(
this.device.deviceInfo.identifier,
appData.projectName,
);
this.$deviceLogProvider.setProjectDirForDevice(
this.device.deviceInfo.identifier,
appData.projectDir,
);

if (!this.$options.justlaunch) {
await this.startDeviceLog();
}
}

@cache()
private startDeviceLog(): Promise<void> {
return this.device.openDeviceLogStream({ predicate: IOS_LOG_PREDICATE });
}
}
40 changes: 40 additions & 0 deletions lib/common/mobile/ios/mac/ios-mac-constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as semver from "semver";

/**
* `--device mac` selects this Mac; it is also the device's imageIdentifier.
*/
export const IOS_MAC_DEVICE_ALIAS = "mac";

/**
* Builds for the Mac go to platforms/ios/build/mac so they never mix with the
* archive and .ipa of a phone build, which share the iphoneos SDK.
*/
export const IOS_MAC_BUILD_DIR_NAME = "mac";

export function isIOSMacDevice(device: Mobile.IDevice): boolean {
return device?.deviceInfo?.imageIdentifier === IOS_MAC_DEVICE_ALIAS;
}

/**
* Whether this @nativescript/ios version loads UIKit from /System/iOSSupport
* on a Mac (NativeScript/ios#551). Older runtimes stop at launch with
* "UIApplicationDidFinishLaunchingNotification is not defined".
* Returns true for versions it can't parse, so only a known-bad one warns.
*/
export function iOSRuntimeCanRunOnMac(version: string): boolean {
const next = /^\d+\.\d+\.\d+-next\.(\d{4}-\d{2}-\d{2})-(\d+)$/.exec(
version ?? "",
);
if (next) {
const [, date, runId] = next;
const firstDate = "2026-11-11";
const firstRunId = 43080423883;
return date > firstDate || (date === firstDate && +runId >= firstRunId);
}

if (!semver.valid(version)) {
return true;
}

return semver.gte(version, "9.1.1");
}
Loading
Loading