Skip to content

Commit 677d581

Browse files
authored
Merge pull request #58 from microsoft/main
Pulling in .net fixes for fusion manifest
2 parents 64027c7 + f972bd6 commit 677d581

File tree

13 files changed

+488
-111
lines changed

13 files changed

+488
-111
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,39 @@ This command will:
6060

6161
> **Tip:** Modify `winsdk.yaml` to change SDK versions, then run `winsdk init` or `winsdk restore` to update your project.
6262
63+
### Hosted Apps (Python/Node.js scripts)
64+
65+
The CLI supports packaging Python and Node.js scripts as MSIX packages using the [Hosted App model](https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/hosted-apps). This allows your scripts to gain Windows app identity and capabilities without bundling a full runtime.
66+
67+
#### Generate a Hosted App manifest
68+
69+
```bash
70+
winsdk manifest generate --template hostedapp --entrypoint app.py
71+
```
72+
73+
This command will:
74+
* ✅ Create an `appxmanifest.xml` configured for hosted apps
75+
* ✅ Auto-detect the script type (Python `.py` or JavaScript `.js`)
76+
* ✅ Configure the appropriate host runtime dependency (Python314 or Nodejs22)
77+
* ✅ Generate required assets in the `Assets` folder
78+
79+
**Supported script types:**
80+
- **Python scripts** (`.py`) - Uses Python314 host runtime
81+
- **JavaScript/Node.js scripts** (`.js`) - Uses Nodejs22 host runtime
82+
83+
#### Debug identity for hosted apps
84+
85+
You can also create debug identity for hosted apps to test them without full MSIX packaging:
86+
87+
```bash
88+
# Generate hosted app manifest
89+
winsdk manifest generate --template hostedapp --entrypoint app.py
90+
91+
# Create debug identity (registers as a sparse package)
92+
winsdk create-debug-identity
93+
```
94+
95+
> **Note:** The hosted app model requires the appropriate runtime (Python 3.14+ or Node.js 22+) to be installed on the target system. The manifest specifies this as a runtime dependency.
6396
6497
### Generate app identity
6598

src/winsdk-CLI/Winsdk.Cli.Tests/ManifestCommandTests.cs

Lines changed: 153 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public async Task ManifestGenerateCommandWithCustomOptionsShouldUseThoseValues()
106106
"--publisher-name", "CN=TestPublisher",
107107
"--version", "2.0.0.0",
108108
"--description", "Test Application",
109-
"--executable", "TestApp.exe",
109+
"--entrypoint", "TestApp.exe",
110110
"--yes" // Skip interactive prompts
111111
};
112112

@@ -138,7 +138,7 @@ public async Task ManifestGenerateCommandWithSparseOptionShouldCreateSparseManif
138138
var args = new[]
139139
{
140140
_tempDirectory,
141-
"--sparse",
141+
"--template", "sparse",
142142
"--yes" // Skip interactive prompts
143143
};
144144

@@ -222,8 +222,8 @@ public void ManifestGenerateCommandParseArgumentsShouldHandleAllOptions()
222222
"--publisher-name", "CN=TestPub",
223223
"--version", "1.2.3.4",
224224
"--description", "Test Description",
225-
"--executable", "test.exe",
226-
"--sparse",
225+
"--entrypoint", "test.exe",
226+
"--template", "sparse",
227227
"--logo-path", "/test/logo.png",
228228
"--yes",
229229
"--verbose"
@@ -356,4 +356,153 @@ public void ManifestGenerateCommandHelpShouldDisplayCorrectInformation()
356356
Assert.IsNotNull(parseResult, "Parse result should not be null");
357357
// The help option should be recognized and not produce errors
358358
}
359+
360+
[TestMethod]
361+
public async Task ManifestGenerateCommandWithHostedAppTemplateShouldCreateHostedAppManifest()
362+
{
363+
// Arrange - Create a Python script file
364+
var pythonScriptPath = Path.Combine(_tempDirectory, "app.py");
365+
await File.WriteAllTextAsync(pythonScriptPath, "# Python script\nprint('Hello, World!')");
366+
367+
var generateCommand = GetRequiredService<ManifestGenerateCommand>();
368+
var args = new[]
369+
{
370+
_tempDirectory,
371+
"--template", "hostedapp",
372+
"--entrypoint", "app.py"
373+
};
374+
375+
// Act
376+
var parseResult = generateCommand.Parse(args);
377+
var exitCode = await parseResult.InvokeAsync();
378+
379+
// Assert
380+
Assert.AreEqual(0, exitCode, "Generate command should complete successfully");
381+
382+
// Verify manifest was created
383+
var manifestPath = Path.Combine(_tempDirectory, "appxmanifest.xml");
384+
Assert.IsTrue(File.Exists(manifestPath), "AppxManifest.xml should be created");
385+
386+
// Verify hosted app specific content
387+
var manifestContent = await File.ReadAllTextAsync(manifestPath);
388+
Assert.Contains("uap10:HostId", manifestContent, "HostedApp manifest should contain HostId");
389+
Assert.Contains("uap10:Parameters", manifestContent, "HostedApp manifest should contain Parameters");
390+
Assert.Contains("uap10:HostRuntimeDependency", manifestContent, "HostedApp manifest should contain HostRuntimeDependency");
391+
Assert.Contains("Python314", manifestContent, "HostedApp manifest should reference Python314 host");
392+
Assert.Contains("app.py", manifestContent, "HostedApp manifest should reference the Python script");
393+
}
394+
395+
[TestMethod]
396+
public async Task CreateDebugIdentityForHostedAppShouldSucceed()
397+
{
398+
// Arrange - Create a Python script file and manifest
399+
var pythonScriptPath = Path.Combine(_tempDirectory, "app.py");
400+
await File.WriteAllTextAsync(pythonScriptPath, "# Python script\nprint('Hello, World!')");
401+
402+
// First, generate a hosted app manifest
403+
var generateCommand = GetRequiredService<ManifestGenerateCommand>();
404+
var generateArgs = new[]
405+
{
406+
_tempDirectory,
407+
"--template", "hostedapp",
408+
"--entrypoint", "app.py"
409+
};
410+
411+
var generateParseResult = generateCommand.Parse(generateArgs);
412+
var generateExitCode = await generateParseResult.InvokeAsync();
413+
Assert.AreEqual(0, generateExitCode, "Manifest generation should succeed");
414+
415+
var manifestPath = Path.Combine(_tempDirectory, "appxmanifest.xml");
416+
Assert.IsTrue(File.Exists(manifestPath), "Manifest should exist");
417+
418+
// Act - Create debug identity
419+
var debugIdentityCommand = GetRequiredService<CreateDebugIdentityCommand>();
420+
var debugArgs = new[]
421+
{
422+
pythonScriptPath,
423+
"--manifest", manifestPath,
424+
"--no-install" // Skip actual installation in test
425+
};
426+
427+
var debugParseResult = debugIdentityCommand.Parse(debugArgs);
428+
var debugExitCode = await debugParseResult.InvokeAsync();
429+
430+
// Assert
431+
Assert.AreEqual(0, debugExitCode, "Create debug identity should complete successfully");
432+
}
433+
434+
[TestMethod]
435+
public async Task ManifestGenerateCommandWithHostedAppTemplateAndJavaScriptShouldSucceed()
436+
{
437+
// Arrange - Create a JavaScript file
438+
var jsScriptPath = Path.Combine(_tempDirectory, "app.js");
439+
await File.WriteAllTextAsync(jsScriptPath, "// JavaScript\nconsole.log('Hello, World!');");
440+
441+
var generateCommand = GetRequiredService<ManifestGenerateCommand>();
442+
var args = new[]
443+
{
444+
_tempDirectory,
445+
"--template", "hostedapp",
446+
"--entrypoint", "app.js"
447+
};
448+
449+
// Act
450+
var parseResult = generateCommand.Parse(args);
451+
var exitCode = await parseResult.InvokeAsync();
452+
453+
// Assert
454+
Assert.AreEqual(0, exitCode, "Generate command should complete successfully");
455+
456+
// Verify manifest was created
457+
var manifestPath = Path.Combine(_tempDirectory, "appxmanifest.xml");
458+
Assert.IsTrue(File.Exists(manifestPath), "AppxManifest.xml should be created");
459+
460+
// Verify hosted app specific content for Node.js
461+
var manifestContent = await File.ReadAllTextAsync(manifestPath);
462+
Assert.Contains("Nodejs22", manifestContent, "HostedApp manifest should reference Nodejs22 host");
463+
Assert.Contains("app.js", manifestContent, "HostedApp manifest should reference the JavaScript file");
464+
}
465+
466+
[TestMethod]
467+
public async Task ManifestGenerateCommandWithHostedAppTemplateAndNonExistentEntryShouldFail()
468+
{
469+
// Arrange - Don't create the Python file
470+
var generateCommand = GetRequiredService<ManifestGenerateCommand>();
471+
var args = new[]
472+
{
473+
_tempDirectory,
474+
"--template", "hostedapp",
475+
"--entrypoint", "nonexistent.py"
476+
};
477+
478+
// Act
479+
var parseResult = generateCommand.Parse(args);
480+
var exitCode = await parseResult.InvokeAsync();
481+
482+
// Assert
483+
Assert.AreNotEqual(0, exitCode, "Generate command should fail when entry point file doesn't exist");
484+
}
485+
486+
[TestMethod]
487+
public async Task ManifestGenerateCommandWithHostedAppTemplateAndUnsupportedTypeShouldFail()
488+
{
489+
// Arrange - Create a file with unsupported extension
490+
var unsupportedFilePath = Path.Combine(_tempDirectory, "app.exe");
491+
await File.WriteAllTextAsync(unsupportedFilePath, "fake exe content");
492+
493+
var generateCommand = GetRequiredService<ManifestGenerateCommand>();
494+
var args = new[]
495+
{
496+
_tempDirectory,
497+
"--template", "hostedapp",
498+
"--entrypoint", "app.exe"
499+
};
500+
501+
// Act
502+
var parseResult = generateCommand.Parse(args);
503+
var exitCode = await parseResult.InvokeAsync();
504+
505+
// Assert
506+
Assert.AreNotEqual(0, exitCode, "Generate command should fail for unsupported hosted app entry point type");
507+
}
359508
}

src/winsdk-CLI/Winsdk.Cli/Commands/CreateDebugIdentityCommand.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,17 @@ namespace Winsdk.Cli.Commands;
1111

1212
internal class CreateDebugIdentityCommand : Command
1313
{
14-
public static Argument<string> ExecutableArgument { get; }
14+
public static Argument<string> EntryPointArgument { get; }
1515
public static Option<string> ManifestOption { get; }
1616
public static Option<bool> NoInstallOption { get; }
1717
public static Option<string> LocationOption { get; }
1818

1919
static CreateDebugIdentityCommand()
2020
{
21-
ExecutableArgument = new Argument<string>("executable")
21+
EntryPointArgument = new Argument<string>("entrypoint")
2222
{
23-
Description = "Path to the .exe that will need to run with identity"
23+
Description = "Path to the .exe that will need to run with identity, or entrypoint script.",
24+
Arity = ArgumentArity.ZeroOrOne
2425
};
2526
ManifestOption = new Option<string>("--manifest")
2627
{
@@ -39,7 +40,7 @@ static CreateDebugIdentityCommand()
3940

4041
public CreateDebugIdentityCommand() : base("create-debug-identity", "Create and install a temporary package for debugging. Must be called every time the appxmanifest.xml is modified for changes to take effect.")
4142
{
42-
Arguments.Add(ExecutableArgument);
43+
Arguments.Add(EntryPointArgument);
4344
Options.Add(ManifestOption);
4445
Options.Add(NoInstallOption);
4546
Options.Add(LocationOption);
@@ -49,20 +50,20 @@ public class Handler(IMsixService msixService, ILogger<CreateDebugIdentityComman
4950
{
5051
public override async Task<int> InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default)
5152
{
52-
var executablePath = parseResult.GetRequiredValue(ExecutableArgument);
53+
var entryPointPath = parseResult.GetValue(EntryPointArgument);
5354
var manifest = parseResult.GetRequiredValue(ManifestOption);
5455
var noInstall = parseResult.GetValue(NoInstallOption);
5556
var location = parseResult.GetValue(LocationOption);
5657

57-
if (!File.Exists(executablePath))
58+
if (entryPointPath != null && !File.Exists(entryPointPath))
5859
{
59-
logger.LogError("Executable not found: {ExecutablePath}", executablePath);
60+
logger.LogError("EntryPoint/Executable not found: {EntryPointPath}", entryPointPath);
6061
return 1;
6162
}
6263

6364
try
6465
{
65-
var result = await msixService.AddMsixIdentityToExeAsync(executablePath, manifest, noInstall, location, cancellationToken);
66+
var result = await msixService.AddMsixIdentityAsync(entryPointPath, manifest, noInstall, location, cancellationToken);
6667

6768
logger.LogInformation("{UISymbol} MSIX identity added successfully!", UiSymbols.Check);
6869
logger.LogInformation("{UISymbol} Package: {PackageName}", UiSymbols.Package, result.PackageName);

src/winsdk-CLI/Winsdk.Cli/Commands/ManifestGenerateCommand.cs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.CommandLine;
66
using System.CommandLine.Invocation;
77
using Winsdk.Cli.Helpers;
8+
using Winsdk.Cli.Models;
89
using Winsdk.Cli.Services;
910

1011
namespace Winsdk.Cli.Commands;
@@ -16,8 +17,8 @@ internal class ManifestGenerateCommand : Command
1617
public static Option<string> PublisherNameOption { get; }
1718
public static Option<string> VersionOption { get; }
1819
public static Option<string> DescriptionOption { get; }
19-
public static Option<string?> ExecutableOption { get; }
20-
public static Option<bool> SparseOption { get; }
20+
public static Option<string?> EntryPointOption { get; }
21+
public static Option<ManifestTemplates> TemplateOption { get; }
2122
public static Option<string?> LogoPathOption { get; }
2223
public static Option<bool> YesOption { get; }
2324

@@ -52,14 +53,15 @@ static ManifestGenerateCommand()
5253
DefaultValueFactory = (argumentResult) => "My Application"
5354
};
5455

55-
ExecutableOption = new Option<string?>("--executable")
56+
EntryPointOption = new Option<string?>("--entrypoint", "--executable")
5657
{
57-
Description = "Executable path/name (default: <package-name>.exe)"
58+
Description = "Entry point of the application (e.g., executable path / name, or .py/.js script if template is HostedApp). Default: <package-name>.exe"
5859
};
5960

60-
SparseOption = new Option<bool>("--sparse")
61+
TemplateOption = new Option<ManifestTemplates>("--template")
6162
{
62-
Description = "Generate sparse package manifest"
63+
Description = "Generate manifest using specified template",
64+
DefaultValueFactory = (argumentResult) => ManifestTemplates.Packaged
6365
};
6466

6567
LogoPathOption = new Option<string?>("--logo-path")
@@ -80,8 +82,8 @@ public ManifestGenerateCommand() : base("generate", "Generate a manifest in dire
8082
Options.Add(PublisherNameOption);
8183
Options.Add(VersionOption);
8284
Options.Add(DescriptionOption);
83-
Options.Add(ExecutableOption);
84-
Options.Add(SparseOption);
85+
Options.Add(EntryPointOption);
86+
Options.Add(TemplateOption);
8587
Options.Add(LogoPathOption);
8688
Options.Add(YesOption);
8789
}
@@ -95,8 +97,8 @@ public override async Task<int> InvokeAsync(ParseResult parseResult, Cancellatio
9597
var publisherName = parseResult.GetValue(PublisherNameOption);
9698
var version = parseResult.GetRequiredValue(VersionOption);
9799
var description = parseResult.GetRequiredValue(DescriptionOption);
98-
var executable = parseResult.GetValue(ExecutableOption);
99-
var sparse = parseResult.GetValue(SparseOption);
100+
var entryPoint = parseResult.GetValue(EntryPointOption);
101+
var template = parseResult.GetValue(TemplateOption);
100102
var logoPath = parseResult.GetValue(LogoPathOption);
101103
var yes = parseResult.GetValue(YesOption);
102104
try
@@ -107,8 +109,8 @@ await manifestService.GenerateManifestAsync(
107109
publisherName,
108110
version,
109111
description,
110-
executable,
111-
sparse,
112+
entryPoint,
113+
template,
112114
logoPath,
113115
yes,
114116
cancellationToken);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
namespace Winsdk.Cli.Models;
5+
6+
public enum ManifestTemplates
7+
{
8+
Packaged,
9+
Sparse,
10+
HostedApp
11+
}

src/winsdk-CLI/Winsdk.Cli/Services/IManifestService.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
using Winsdk.Cli.Models;
5+
46
namespace Winsdk.Cli.Services;
57

68
internal interface IManifestService
@@ -11,8 +13,8 @@ public Task GenerateManifestAsync(
1113
string? publisherName,
1214
string version,
1315
string description,
14-
string? executable,
15-
bool sparse,
16+
string? entryPoint,
17+
ManifestTemplates manifestTemplate,
1618
string? logoPath,
1719
bool yes,
1820
CancellationToken cancellationToken = default);

0 commit comments

Comments
 (0)