Skip to content

Commit 95cc0ed

Browse files
committed
feat(windows): release
1 parent 7a54c25 commit 95cc0ed

7 files changed

Lines changed: 161 additions & 26 deletions

File tree

lib/commands/build.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
22
ANDROID_RELEASE_BUILD_ERROR_MESSAGE,
33
AndroidAppBundleMessages,
4+
WINDOWS_RELEASE_UNSIGNED_MESSAGE,
5+
WindowsAppPackageMessages,
46
} from "../constants";
57
import { ValidatePlatformCommandBase } from "./command-base";
68
import { hasValidAndroidSigning } from "../common/helpers";
@@ -310,6 +312,10 @@ export class BuildWindowsCommand extends BuildCommandBase implements ICommand {
310312
await this.executeCore([
311313
this.$devicePlatformsConstants.Windows.toLowerCase(),
312314
]);
315+
316+
if (this.$options.release && this.$options.storeUpload) {
317+
this.$logger.info(WindowsAppPackageMessages.STORE_UPLOAD_DOCS_MESSAGE);
318+
}
313319
}
314320

315321
public async canExecute(args: string[]): Promise<boolean> {
@@ -323,6 +329,17 @@ export class BuildWindowsCommand extends BuildCommandBase implements ICommand {
323329

324330
let canExecute = await super.canExecuteCommandBase(platform);
325331
if (canExecute) {
332+
// A sideload release package without signing material can be produced,
333+
// but it won't be installable until signed — warn rather than fail.
334+
if (
335+
this.$options.release &&
336+
!this.$options.storeUpload &&
337+
!this.$options.certificate &&
338+
!this.$options.certificateThumbprint
339+
) {
340+
this.$logger.warn(WINDOWS_RELEASE_UNSIGNED_MESSAGE);
341+
}
342+
326343
canExecute = await super.validateArgs(args, platform);
327344
}
328345

lib/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,12 @@ const ANDROID_SIGNING_REQUIRED_MESSAGE =
218218
"you need to specify all --key-store-* options.";
219219
export const ANDROID_RELEASE_BUILD_ERROR_MESSAGE = `When producing a release build, ${ANDROID_SIGNING_REQUIRED_MESSAGE}`;
220220
export const ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE = `When producing Android App Bundle, ${ANDROID_SIGNING_REQUIRED_MESSAGE}`;
221+
export const WINDOWS_RELEASE_UNSIGNED_MESSAGE =
222+
"Producing an unsigned Windows release package. The resulting .msix cannot be installed directly until it is signed. Pass --certificate (and --certificate-password) or --certificate-thumbprint to produce a signed, sideloadable package, or use --store-upload to produce a package for submission to the Microsoft Store.";
223+
export class WindowsAppPackageMessages {
224+
public static STORE_UPLOAD_DOCS_MESSAGE =
225+
"Built a Microsoft Store upload package (.msixupload). Submit it to your app via Partner Center: https://partner.microsoft.com/dashboard";
226+
}
221227
export const CACACHE_DIRECTORY_NAME = "_cacache";
222228

223229
export const FILES_CHANGE_EVENT_NAME = "filesChangeEvent";

lib/data/build-data.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,28 @@ export class IOSBuildData extends BuildData implements IiOSBuildData {
4545
}
4646
}
4747

48+
export class WindowsBuildData extends BuildData {
49+
public certificate: string;
50+
public certificatePassword: string;
51+
public certificateThumbprint: string;
52+
public storeUpload: boolean;
53+
public msixBundle: boolean;
54+
public architectures: string[];
55+
56+
constructor(projectDir: string, platform: string, data: any) {
57+
super(projectDir, platform, data);
58+
59+
this.certificate = data.certificate;
60+
this.certificatePassword = data.certificatePassword;
61+
this.certificateThumbprint = data.certificateThumbprint;
62+
this.storeUpload = data.storeUpload;
63+
this.msixBundle = data.msixBundle || data.msixbundle;
64+
this.architectures = data.arch
65+
? [data.arch]
66+
: data.architectures ?? ["x64"];
67+
}
68+
}
69+
4870
export class AndroidBuildData extends BuildData {
4971
public keyStoreAlias: string;
5072
public keyStorePath: string;

lib/declarations.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,10 @@ interface IOptions
661661
template: string;
662662
certificate: string;
663663
certificatePassword: string;
664+
certificateThumbprint: string;
665+
storeUpload: boolean;
666+
msixbundle: boolean;
667+
arch: string;
664668
var: Object;
665669
default: Boolean;
666670
count: number;

lib/options.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,13 @@ export class Options {
204204
template: { type: OptionType.String, hasSensitiveValue: true },
205205
certificate: { type: OptionType.String, hasSensitiveValue: true },
206206
certificatePassword: { type: OptionType.String, hasSensitiveValue: true },
207+
certificateThumbprint: {
208+
type: OptionType.String,
209+
hasSensitiveValue: true,
210+
},
211+
storeUpload: { type: OptionType.Boolean, hasSensitiveValue: false },
212+
msixbundle: { type: OptionType.Boolean, hasSensitiveValue: false },
213+
arch: { type: OptionType.String, hasSensitiveValue: false },
207214
release: {
208215
type: OptionType.Boolean,
209216
alias: "r",

lib/services/build-data-service.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { AndroidBuildData, BuildData, IOSBuildData } from "../data/build-data";
1+
import {
2+
AndroidBuildData,
3+
IOSBuildData,
4+
WindowsBuildData,
5+
} from "../data/build-data";
26
import { IBuildDataService } from "../definitions/build";
37
import { injector } from "../common/yok";
48

@@ -11,7 +15,7 @@ export class BuildDataService implements IBuildDataService {
1115
} else if (this.$mobileHelper.isAndroidPlatform(platform)) {
1216
return new AndroidBuildData(projectDir, platform, data);
1317
} else if (this.$mobileHelper.isWindowsPlatform(platform)) {
14-
return new BuildData(projectDir, platform, data);
18+
return new WindowsBuildData(projectDir, platform, data);
1519
}
1620
}
1721
}

lib/services/windows-project-service.ts

Lines changed: 99 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -114,18 +114,40 @@ export class WindowsProjectService
114114
),
115115
platformProjectService: <any>this,
116116
projectRoot: projectRoot,
117-
getBuildOutputPath: (_options?: any): string => {
117+
getBuildOutputPath: (options?: any): string => {
118+
// Release builds are packaged into AppPackages/ (.msix/.msixupload),
119+
// while debug builds run from the plain `dotnet build` output in bin/.
120+
if (options?.release) {
121+
return path.join(
122+
projectRoot,
123+
projectData.projectName,
124+
"AppPackages",
125+
);
126+
}
118127
return path.join(projectRoot, projectData.projectName, "bin");
119128
},
120-
getValidBuildOutputData: (): IValidBuildOutputData => {
129+
getValidBuildOutputData: (
130+
options?: any,
131+
): IValidBuildOutputData => {
132+
if (options?.release) {
133+
// Packaged artifacts live in versioned subfolders of AppPackages/
134+
// (e.g. App_1.0.0.0_x64_Test/App_1.0.0.0_x64.msix), so match by
135+
// extension rather than an exact file name. A representative
136+
// package name is kept so callers that read packageNames[0]
137+
// (e.g. for error messages) have a valid value.
138+
return {
139+
packageNames: [
140+
`${projectData.projectName}.msixupload`,
141+
`${projectData.projectName}.msix`,
142+
],
143+
regexes: [
144+
/\.(msixupload|appxupload|msixbundle|appxbundle|msix|appx)$/i,
145+
],
146+
};
147+
}
148+
// AppxManifest.xml triggers Add-AppxPackage -Register (dev flow).
121149
return {
122-
// AppxManifest.xml triggers Add-AppxPackage -Register (dev flow).
123-
// msix/appx handle release builds.
124-
packageNames: [
125-
"AppxManifest.xml",
126-
`${projectData.projectName}.msix`,
127-
`${projectData.projectName}.appx`,
128-
],
150+
packageNames: ["AppxManifest.xml"],
129151
};
130152
},
131153
configurationFileName: "Package.appxmanifest",
@@ -275,19 +297,25 @@ export class WindowsProjectService
275297
projectData: IProjectData,
276298
buildConfig: any,
277299
): Promise<void> {
278-
const config = buildConfig?.release
279-
? Configurations.Release
280-
: Configurations.Debug;
300+
const isRelease = !!buildConfig?.release;
301+
const config = isRelease ? Configurations.Release : Configurations.Debug;
281302
const arch = buildConfig?.architectures?.[0] ?? "x64";
282303
const csproj = path.join(
283304
projectRoot,
284305
projectData.projectName,
285306
`${projectData.projectName}.csproj`,
286307
);
287308
const outputPath = path.join(projectRoot, projectData.projectName, "bin");
309+
const appPackagesDir = path.join(
310+
projectRoot,
311+
projectData.projectName,
312+
"AppPackages",
313+
);
288314

289315
this.$logger.info(
290-
`Building Windows project: ${csproj} [${config}|${arch}]`,
316+
`Building Windows project: ${csproj} [${config}|${arch}]${
317+
isRelease ? " (release package)" : ""
318+
}`,
291319
);
292320

293321
// If the app project provides an App_Resources/Windows/Package.appxmanifest,
@@ -323,15 +351,61 @@ export class WindowsProjectService
323351
}
324352
}
325353

326-
const dotnetArgs = [
327-
"build",
328-
csproj,
329-
"-c",
330-
config,
331-
`-p:Platform=${arch}`,
332-
"--output",
333-
outputPath,
334-
];
354+
const dotnetArgs = ["build", csproj, "-c", config, `-p:Platform=${arch}`];
355+
356+
if (isRelease) {
357+
// Produce an MSIX package (sideload-signed by default) or a Microsoft
358+
// Store upload bundle when --store-upload is passed, instead of a plain
359+
// run-from-bin build.
360+
const storeUpload = !!buildConfig?.storeUpload;
361+
const bundle = !!buildConfig?.msixBundle || storeUpload;
362+
const certificate: string = buildConfig?.certificate;
363+
const certificatePassword: string = buildConfig?.certificatePassword;
364+
const certificateThumbprint: string =
365+
buildConfig?.certificateThumbprint;
366+
367+
dotnetArgs.push(
368+
"-p:GenerateAppxPackageOnBuild=true",
369+
`-p:AppxPackageDir=${appPackagesDir}${path.sep}`,
370+
`-p:UapAppxPackageBuildMode=${
371+
storeUpload ? "StoreUpload" : "SideloadOnly"
372+
}`,
373+
`-p:AppxBundle=${bundle ? "Always" : "Never"}`,
374+
);
375+
376+
if (bundle) {
377+
dotnetArgs.push(`-p:AppxBundlePlatforms=${arch}`);
378+
}
379+
380+
if (storeUpload) {
381+
// The Store re-signs the package on submission, so don't sign locally.
382+
dotnetArgs.push("-p:AppxPackageSigningEnabled=false");
383+
} else if (certificateThumbprint) {
384+
dotnetArgs.push(
385+
"-p:AppxPackageSigningEnabled=true",
386+
`-p:PackageCertificateThumbprint=${certificateThumbprint}`,
387+
);
388+
} else if (certificate) {
389+
const certPath = path.isAbsolute(certificate)
390+
? certificate
391+
: path.join(projectData.projectDir, certificate);
392+
dotnetArgs.push(
393+
"-p:AppxPackageSigningEnabled=true",
394+
`-p:PackageCertificateKeyFile=\"${certPath}\"`,
395+
);
396+
if (certificatePassword) {
397+
dotnetArgs.push(
398+
`-p:PackageCertificatePassword=${certificatePassword}`,
399+
);
400+
}
401+
} else {
402+
// No signing material provided — emit an unsigned package.
403+
dotnetArgs.push("-p:AppxPackageSigningEnabled=false");
404+
}
405+
} else {
406+
dotnetArgs.push("--output", outputPath);
407+
}
408+
335409
if (appResourcesManifestArg) {
336410
dotnetArgs.push(appResourcesManifestArg);
337411
}
@@ -354,14 +428,15 @@ export class WindowsProjectService
354428
}
355429

356430
// Add-AppxPackage -Register requires the manifest to be named AppxManifest.xml
357-
// with $targetnametoken$ expanded to the actual EXE name.
431+
// with $targetnametoken$ expanded to the actual EXE name. This is only used by
432+
// the dev (debug) register flow; release builds produce a packaged .msix.
358433
const manifestSrc = path.join(
359434
projectRoot,
360435
projectData.projectName,
361436
"Package.appxmanifest",
362437
);
363438
const manifestDest = path.join(outputPath, "AppxManifest.xml");
364-
if (this.$fs.exists(manifestSrc)) {
439+
if (!isRelease && this.$fs.exists(manifestSrc)) {
365440
const raw = fs.readFileSync(manifestSrc, "utf8");
366441
const expanded = raw.split("$targetnametoken$").join(projectData.projectName);
367442
fs.writeFileSync(manifestDest, expanded, "utf8");

0 commit comments

Comments
 (0)