Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/device-targeted-install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@oniroproject/core": minor
"@oniroproject/oniro-app": minor
---

Add device targeting to app installation. `installApp` now accepts `deviceSerial`, and `oniro-app app install` exposes it as `--device <serial>`.
5 changes: 3 additions & 2 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ These commands target a connected device or emulator over `hdc`. Most accept `--

| Command | Description |
| --- | --- |
| `app install [project-dir] [--hap <path>]` | Install the signed `.hap` via `hdc`. `--hap` overrides the resolved path. |
| `app launch [project-dir] [--module <m>] [--ability <name>]` | Launch the app. `--ability` defaults to the module's `mainElement` / first visible ability. |
| `app install [project-dir] [--hap <path>] [--device <serial>]` | Install the signed `.hap` via `hdc`. `--hap` overrides the resolved path. |
| `app launch [project-dir] [--module <m>] [--ability <name>] [--device <serial>]` | Launch the app. `--ability` defaults to the module's `mainElement` / first visible ability. |
| `app apply [project-dir] --bundle <b> [options]` | Install a HAP **and verify the running process took the change** — handles sign-info mismatch, asset-cache invalidation, and persistent-bundle restart. |
| `app uninstall <bundle> [--device <serial>]` | Uninstall an app by bundle name. |
| `app stop <bundle> [--device <serial>]` | Force-stop an app by bundle name. |
Expand All @@ -178,6 +178,7 @@ These commands target a connected device or emulator over `hdc`. Most accept `--
| `--installed-hap <path>` | Local copy of the currently-installed HAP, to enable the asset-cache reboot diff. |
| `--system` | Treat as a persistent/system bundle (refuses to uninstall on a sign-info mismatch). |
| `--allow-uninstall` | Permit uninstalling a system bundle on sign-info mismatch (dangerous). |
| `--device <serial>` | Target device serial. |
| `--json` | Emit the result (`method`, pre/post-install pid, `cacheCleared`) as JSON. |

#### `file` — transfer files to/from the device
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ export function registerAppCommand(program: Command): void {
.command('install [project-dir]')
.description('Install the signed .hap on the connected device/emulator via hdc.')
.option('--hap <path>', 'Override the .hap path (relative to project-dir or absolute).')
.action(async (projectDir: string | undefined, opts: { hap?: string }) => {
.option('--device <serial>', 'Target device serial.')
.action(async (projectDir: string | undefined, opts: { hap?: string; device?: string }) => {
const { config, logger } = getRuntime();
const dir = path.resolve(projectDir ?? process.cwd());
logger.info(`Installing app from ${dir}...`);
await installApp({ config, projectDir: dir, hapPath: opts.hap, logger });
await installApp({ config, projectDir: dir, hapPath: opts.hap, deviceSerial: opts.device, logger });
logger.info('App installed.');
});

Expand Down
46 changes: 46 additions & 0 deletions packages/cli/tests/app.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as path from 'node:path';
import { Command } from 'commander';

const mocks = vi.hoisted(() => ({
installApp: vi.fn(),
}));

vi.mock('@oniroproject/core', async () => {
const actual = await vi.importActual<typeof import('@oniroproject/core')>('@oniroproject/core');
return { ...actual, installApp: mocks.installApp };
});

import { noopLogger, noopProgress, staticConfig } from '@oniroproject/core';
import { registerAppCommand } from '../src/commands/app.js';
import { setRuntime } from '../src/lib/runtime.js';

describe('app install', () => {
beforeEach(() => {
mocks.installApp.mockReset();
mocks.installApp.mockResolvedValue({ installed: true, bundleName: '', hapPath: '', output: '' });
setRuntime({ config: staticConfig(), logger: noopLogger, progress: noopProgress });
});

afterEach(() => {
setRuntime(null);
});

it('forwards --device to installApp', async () => {
const program = new Command();
registerAppCommand(program);

await program.parseAsync(
['app', 'install', 'project', '--hap', 'app.hap', '--device', 'ABC123'],
{ from: 'user' },
);

expect(mocks.installApp).toHaveBeenCalledWith({
config: expect.any(Object),
projectDir: path.resolve('project'),
hapPath: 'app.hap',
deviceSerial: 'ABC123',
logger: noopLogger,
});
});
});
8 changes: 7 additions & 1 deletion packages/core/src/hdc/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface InstallAppOptions {
* Falls back to the `hapPath` config key, then to the well-known default.
*/
hapPath?: string;
deviceSerial?: string;
logger?: Logger;
}

Expand All @@ -41,7 +42,12 @@ export async function installApp(opts: InstallAppOptions): Promise<InstallAppRes
throw new OniroError(`HAP file not found at: ${hapPath}. Build and sign the app first.`);
}

const result = await hdcExec({ config: opts.config, args: ['install', hapPath], timeoutMs: 300_000 });
const result = await hdcExec({
config: opts.config,
args: ['install', hapPath],
deviceSerial: opts.deviceSerial,
timeoutMs: 300_000,
});
if (result.stdout.trim()) logger.info(result.stdout.trim());
if (result.stderr.trim()) logger.warn(result.stderr.trim());
ensureOk(result, `hdc install ${hapPath}`);
Expand Down
6 changes: 3 additions & 3 deletions packages/core/tests/exec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,11 @@ describe.skipIf(isWin)('installApp / launchApp argv + injection safety (fake hdc

const config = () => staticConfig({ cmdToolsPath: root });

it('installApp sends `install <hapPath>` as discrete args', async () => {
it('installApp sends the target and `install <hapPath>` as discrete args', async () => {
const hapPath = path.join(projectDir, 'app.hap');
fs.writeFileSync(hapPath, 'fake');
await installApp({ config: config(), projectDir, hapPath });
// No throw means exit 0 from the fake hdc. (Output is logged, not returned in P1.)
const result = await installApp({ config: config(), projectDir, hapPath, deviceSerial: 'ABC123' });
expect(result.output.split('\n').filter(Boolean)).toEqual(['-t', 'ABC123', 'install', hapPath]);
});

it('launchApp passes a metacharacter-laden ability as ONE argv element (no host injection)', async () => {
Expand Down
Loading