commit 48b8303521a8394c06c476056d8acbaba4be16af parent 9bc2c1145d752307b06b3f3bcc7249da2c837585 Author: Chacón <lechacon@users.noreply.github.com> Date: Fri, 20 Aug 2021 16:56:38 -0700 Show license agreements when installing (#1347) Diffstat:
54 files changed, 2499 insertions(+), 180 deletions(-)
diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -120,6 +120,7 @@ errstr esrp etag ETW +EULA EVENTTAG exe executables diff --git a/schemas/JSON/manifests/v1.1.0/manifest.defaultLocale.1.1.0.json b/schemas/JSON/manifests/v1.1.0/manifest.defaultLocale.1.1.0.json @@ -0,0 +1,171 @@ +{ + "$id": "https://aka.ms/winget-manifest.defaultlocale.1.1.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A representation of a multiple-file manifest representing a default app metadata in the OWC. v1.1.0", + "definitions": { + "Url": { + "type": [ "string", "null" ], + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://.+$", + "maxLength": 2048, + "description": "Optional Url type" + }, + "Tag": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 40, + "description": "Package moniker or tag" + }, + "Agreement": { + "type": "object", + "properties": { + "AgreementLabel": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "The label of the Agreement. i.e. EULA, AgeRating, etc. This field should be localized. Either Agreement or AgreementUrl is required. When we show the agreements, we would Bold the AgreementLabel" + }, + "Agreement": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 10000, + "description": "The agreement text content." + }, + "AgreementUrl": { + "$ref": "#/definitions/Url", + "description": "The agreement URL." + } + } + } + }, + "type": "object", + "properties": { + "PackageIdentifier": { + "type": "string", + "pattern": "^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,3}$", + "maxLength": 128, + "description": "The package unique identifier" + }, + "PackageVersion": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 128, + "description": "The package version" + }, + "PackageLocale": { + "type": "string", + "default": "en-US", + "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", + "maxLength": 20, + "description": "The package meta-data locale" + }, + "Publisher": { + "type": "string", + "minLength": 2, + "maxLength": 256, + "description": "The publisher name" + }, + "PublisherUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher home page" + }, + "PublisherSupportUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher support page" + }, + "PrivacyUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher privacy page or the package privacy page" + }, + "Author": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 256, + "description": "The package author" + }, + "PackageName": { + "type": "string", + "minLength": 2, + "maxLength": 256, + "description": "The package name" + }, + "PackageUrl": { + "$ref": "#/definitions/Url", + "description": "The package home page" + }, + "License": { + "type": "string", + "minLength": 3, + "maxLength": 512, + "description": "The package license" + }, + "LicenseUrl": { + "$ref": "#/definitions/Url", + "description": "The license page" + }, + "Copyright": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 512, + "description": "The package copyright" + }, + "CopyrightUrl": { + "$ref": "#/definitions/Url", + "description": "The package copyright page" + }, + "ShortDescription": { + "type": "string", + "minLength": 3, + "maxLength": 256, + "description": "The short package description" + }, + "Description": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 10000, + "description": "The full package description" + }, + "Moniker": { + "$ref": "#/definitions/Tag", + "description": "The most common package term" + }, + "Tags": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/Tag" + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of additional package search terms" + }, + "Agreements": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/Agreement" + }, + "maxItems": 128 + }, + "ManifestType": { + "type": "string", + "default": "defaultLocale", + "const": "defaultLocale", + "description": "The manifest type" + }, + "ManifestVersion": { + "type": "string", + "default": "1.1.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + } + }, + "required": [ + "PackageIdentifier", + "PackageVersion", + "PackageLocale", + "Publisher", + "PackageName", + "License", + "ShortDescription", + "ManifestType", + "ManifestVersion" + ] +}+ \ No newline at end of file diff --git a/schemas/JSON/manifests/v1.1.0/manifest.installer.1.1.0.json b/schemas/JSON/manifests/v1.1.0/manifest.installer.1.1.0.json @@ -0,0 +1,457 @@ +{ + "$id": "https://aka.ms/winget-manifest.installer.1.1.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A representation of a single-file manifest representing an app installers in the OWC. v1.1.0", + "definitions": { + "PackageIdentifier": { + "type": "string", + "pattern": "^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,3}$", + "maxLength": 128, + "description": "The package unique identifier" + }, + "PackageVersion": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 128, + "description": "The package version" + }, + "Locale": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 20, + "description": "The package meta-data locale" + }, + "Channel": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 16, + "description": "The distribution channel" + }, + "Platform": { + "type": [ "array", "null" ], + "items": { + "title": "Platform", + "type": "string", + "enum": [ + "Windows.Desktop", + "Windows.Universal" + ] + }, + "maxItems": 2, + "uniqueItems": true, + "description": "The installer supported operating system" + }, + "MinimumOSVersion": { + "type": [ "string", "null" ], + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){0,3}$", + "description": "The installer minimum operating system version" + }, + "InstallerType": { + "type": [ "string", "null" ], + "enum": [ + "msix", + "msi", + "appx", + "exe", + "zip", + "inno", + "nullsoft", + "wix", + "burn", + "pwa" + ], + "description": "Enumeration of supported installer types. InstallerType is required in either root level or individual Installer level" + }, + "Scope": { + "type": [ "string", "null" ], + "enum": [ + "user", + "machine" + ], + "description": "Scope indicates if the installer is per user or per machine" + }, + "InstallModes": { + "type": [ "array", "null" ], + "items": { + "title": "InstallModes", + "type": "string", + "enum": [ + "interactive", + "silent", + "silentWithProgress" + ] + }, + "maxItems": 3, + "uniqueItems": true, + "description": "List of supported installer modes" + }, + "InstallerSwitches": { + "type": "object", + "properties": { + "Silent": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Silent is the value that should be passed to the installer when user chooses a silent or quiet install" + }, + "SilentWithProgress": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "SilentWithProgress is the value that should be passed to the installer when user chooses a non-interactive install" + }, + "Interactive": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Interactive is the value that should be passed to the installer when user chooses an interactive install" + }, + "InstallLocation": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "InstallLocation is the value passed to the installer for custom install location. <INSTALLPATH> token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Log": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Log is the value passed to the installer for custom log file path. <LOGPATH> token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Upgrade": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Upgrade is the value that should be passed to the installer when user chooses an upgrade" + }, + "Custom": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 2048, + "description": "Custom switches will be passed directly to the installer by winget" + } + } + }, + "InstallerSuccessCodes": { + "type": [ "array", "null" ], + "items": { + "type": "integer", + "not": { + "enum": [ 0 ] + } + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of additional non-zero installer success exit codes other than known default values by winget" + }, + "UpgradeBehavior": { + "type": [ "string", "null" ], + "enum": [ + "install", + "uninstallPrevious" + ], + "description": "The upgrade method" + }, + "Commands": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of commands or aliases to run the package" + }, + "Protocols": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "pattern": "^[a-z][-a-z0-9\\.\\+]*$", + "maxLength": 2048 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of protocols the package provides a handler for" + }, + "FileExtensions": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 64 + }, + "maxItems": 256, + "uniqueItems": true, + "description": "List of file extensions the package could support" + }, + "Dependencies": { + "type": [ "object", "null" ], + "properties": { + "WindowsFeatures": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of Windows feature dependencies" + }, + "WindowsLibraries": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of Windows library dependencies" + }, + "PackageDependencies": { + "type": [ "array", "null" ], + "items": { + "type": "object", + "properties": { + "PackageIdentifier": { + "$ref": "#/definitions/PackageIdentifier" + }, + "MinimumVersion": { + "$ref": "#/definitions/PackageVersion" + } + }, + "required": [ "PackageIdentifier" ] + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of package dependencies from current source" + }, + "ExternalDependencies": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of external package dependencies" + } + } + }, + "PackageFamilyName": { + "type": [ "string", "null" ], + "pattern": "^[A-Za-z0-9][-\\.A-Za-z0-9]+_[A-Za-z0-9]{13}$", + "maxLength": 255, + "description": "PackageFamilyName for appx or msix installer. Could be used for correlation of packages across sources" + }, + "ProductCode": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 255, + "description": "ProductCode could be used for correlation of packages across sources" + }, + "Capabilities": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 1000, + "uniqueItems": true, + "description": "List of appx or msix installer capabilities" + }, + "RestrictedCapabilities": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 1000, + "uniqueItems": true, + "description": "List of appx or msix installer restricted capabilities" + }, + "Installer": { + "type": "object", + "properties": { + "InstallerLocale": { + "$ref": "#/definitions/Locale" + }, + "Platform": { + "$ref": "#/definitions/Platform" + }, + "MinimumOSVersion": { + "$ref": "#/definitions/MinimumOSVersion" + }, + "Architecture": { + "type": "string", + "enum": [ + "x86", + "x64", + "arm", + "arm64", + "neutral" + ], + "description": "The installer target architecture" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "Scope": { + "$ref": "#/definitions/Scope" + }, + "InstallerUrl": { + "type": "string", + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://.+$", + "maxLength": 2048, + "description": "The installer Url" + }, + "InstallerSha256": { + "type": "string", + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "Sha256 is required. Sha256 of the installer" + }, + "SignatureSha256": { + "type": [ "string", "null" ], + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "SignatureSha256 is recommended for appx or msix. It is the sha256 of signature file inside appx or msix. Could be used during streaming install if applicable" + }, + "InstallModes": { + "$ref": "#/definitions/InstallModes" + }, + "InstallerSwitches": { + "$ref": "#/definitions/InstallerSwitches" + }, + "InstallerSuccessCodes": { + "$ref": "#/definitions/InstallerSuccessCodes" + }, + "UpgradeBehavior": { + "$ref": "#/definitions/UpgradeBehavior" + }, + "Commands": { + "$ref": "#/definitions/Commands" + }, + "Protocols": { + "$ref": "#/definitions/Protocols" + }, + "FileExtensions": { + "$ref": "#/definitions/FileExtensions" + }, + "Dependencies": { + "$ref": "#/definitions/Dependencies" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Capabilities": { + "$ref": "#/definitions/Capabilities" + }, + "RestrictedCapabilities": { + "$ref": "#/definitions/RestrictedCapabilities" + } + }, + "required": [ + "Architecture", + "InstallerUrl", + "InstallerSha256" + ] + } + }, + "type": "object", + "properties": { + "PackageIdentifier": { + "$ref": "#/definitions/PackageIdentifier" + }, + "PackageVersion": { + "$ref": "#/definitions/PackageVersion" + }, + "Channel": { + "$ref": "#/definitions/Channel" + }, + "InstallerLocale": { + "$ref": "#/definitions/Locale" + }, + "Platform": { + "$ref": "#/definitions/Platform" + }, + "MinimumOSVersion": { + "$ref": "#/definitions/MinimumOSVersion" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "Scope": { + "$ref": "#/definitions/Scope" + }, + "InstallModes": { + "$ref": "#/definitions/InstallModes" + }, + "InstallerSwitches": { + "$ref": "#/definitions/InstallerSwitches" + }, + "InstallerSuccessCodes": { + "$ref": "#/definitions/InstallerSuccessCodes" + }, + "UpgradeBehavior": { + "$ref": "#/definitions/UpgradeBehavior" + }, + "Commands": { + "$ref": "#/definitions/Commands" + }, + "Protocols": { + "$ref": "#/definitions/Protocols" + }, + "FileExtensions": { + "$ref": "#/definitions/FileExtensions" + }, + "Dependencies": { + "$ref": "#/definitions/Dependencies" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Capabilities": { + "$ref": "#/definitions/Capabilities" + }, + "RestrictedCapabilities": { + "$ref": "#/definitions/RestrictedCapabilities" + }, + "Installers": { + "type": "array", + "items": { + "$ref": "#/definitions/Installer" + }, + "minItems": 1, + "maxItems": 128 + }, + "ManifestType": { + "type": "string", + "default": "installer", + "const": "installer", + "description": "The manifest type" + }, + "ManifestVersion": { + "type": "string", + "default": "1.1.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + } + }, + "required": [ + "PackageIdentifier", + "PackageVersion", + "Installers", + "ManifestType", + "ManifestVersion" + ] +}+ \ No newline at end of file diff --git a/schemas/JSON/manifests/v1.1.0/manifest.locale.1.1.0.json b/schemas/JSON/manifests/v1.1.0/manifest.locale.1.1.0.json @@ -0,0 +1,166 @@ +{ + "$id": "https://aka.ms/winget-manifest.locale.1.1.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A representation of a multiple-file manifest representing app metadata in other locale in the OWC. v1.1.0", + "definitions": { + "Url": { + "type": [ "string", "null" ], + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://.+$", + "maxLength": 2048, + "description": "Optional Url type" + }, + "Tag": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 40, + "description": "Package moniker or tag" + }, + "Agreement": { + "type": "object", + "properties": { + "AgreementLabel": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "The label of the Agreement. i.e. EULA, AgeRating, etc. This field should be localized. Either Agreement or AgreementUrl is required. When we show the agreements, we would Bold the AgreementLabel" + }, + "Agreement": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 10000, + "description": "The agreement text content." + }, + "AgreementUrl": { + "$ref": "#/definitions/Url", + "description": "The agreement URL." + } + } + } + }, + "type": "object", + "properties": { + "PackageIdentifier": { + "type": "string", + "pattern": "^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,3}$", + "maxLength": 128, + "description": "The package unique identifier" + }, + "PackageVersion": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 128, + "description": "The package version" + }, + "PackageLocale": { + "type": "string", + "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", + "maxLength": 20, + "description": "The package meta-data locale" + }, + "Publisher": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 256, + "description": "The publisher name" + }, + "PublisherUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher home page" + }, + "PublisherSupportUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher support page" + }, + "PrivacyUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher privacy page or the package privacy page" + }, + "Author": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 256, + "description": "The package author" + }, + "PackageName": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 256, + "description": "The package name" + }, + "PackageUrl": { + "$ref": "#/definitions/Url", + "description": "The package home page" + }, + "License": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 512, + "description": "The package license" + }, + "LicenseUrl": { + "$ref": "#/definitions/Url", + "description": "The license page" + }, + "Copyright": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 512, + "description": "The package copyright" + }, + "CopyrightUrl": { + "$ref": "#/definitions/Url", + "description": "The package copyright page" + }, + "ShortDescription": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 256, + "description": "The short package description" + }, + "Description": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 10000, + "description": "The full package description" + }, + "Moniker": { + "$ref": "#/definitions/Tag", + "description": "The most common package term" + }, + "Tags": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/Tag" + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of additional package search terms" + }, + "Agreements": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/Agreement" + }, + "maxItems": 128 + }, + "ManifestType": { + "type": "string", + "default": "locale", + "const": "locale", + "description": "The manifest type" + }, + "ManifestVersion": { + "type": "string", + "default": "1.1.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + } + }, + "required": [ + "PackageIdentifier", + "PackageVersion", + "PackageLocale", + "ManifestType", + "ManifestVersion" + ] +}+ \ No newline at end of file diff --git a/schemas/JSON/manifests/v1.1.0/manifest.singleton.1.1.0.json b/schemas/JSON/manifests/v1.1.0/manifest.singleton.1.1.0.json @@ -0,0 +1,586 @@ +{ + "$id": "https://aka.ms/winget-manifest.singleton.1.1.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A representation of a single-file manifest representing an app in the OWC. v1.1.0", + "definitions": { + "PackageIdentifier": { + "type": "string", + "pattern": "^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,3}$", + "maxLength": 128, + "description": "The package unique identifier" + }, + "PackageVersion": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 128, + "description": "The package version" + }, + "Locale": { + "type": [ "string", "null" ], + "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", + "maxLength": 20, + "description": "The package meta-data locale" + }, + "Url": { + "type": [ "string", "null" ], + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://.+$", + "maxLength": 2048, + "description": "Optional Url type" + }, + "Tag": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 40, + "description": "Package moniker or tag" + }, + "Agreement": { + "type": "object", + "properties": { + "AgreementLabel": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "The label of the Agreement. i.e. EULA, AgeRating, etc. This field should be localized. Either Agreement or AgreementUrl is required. When we show the agreements, we would Bold the AgreementLabel" + }, + "Agreement": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 10000, + "description": "The agreement text content." + }, + "AgreementUrl": { + "$ref": "#/definitions/Url", + "description": "The agreement URL." + } + } + }, + "Channel": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 16, + "description": "The distribution channel" + }, + "Platform": { + "type": [ "array", "null" ], + "items": { + "title": "Platform", + "type": "string", + "enum": [ + "Windows.Desktop", + "Windows.Universal" + ] + }, + "maxItems": 2, + "uniqueItems": true, + "description": "The installer supported operating system" + }, + "MinimumOSVersion": { + "type": [ "string", "null" ], + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){0,3}$", + "description": "The installer minimum operating system version" + }, + "InstallerType": { + "type": [ "string", "null" ], + "enum": [ + "msix", + "msi", + "appx", + "exe", + "zip", + "inno", + "nullsoft", + "wix", + "burn", + "pwa" + ], + "description": "Enumeration of supported installer types. InstallerType is required in either root level or individual Installer level" + }, + "Scope": { + "type": [ "string", "null" ], + "enum": [ + "user", + "machine" + ], + "description": "Scope indicates if the installer is per user or per machine" + }, + "InstallModes": { + "type": [ "array", "null" ], + "items": { + "title": "InstallModes", + "type": "string", + "enum": [ + "interactive", + "silent", + "silentWithProgress" + ] + }, + "maxItems": 3, + "uniqueItems": true, + "description": "List of supported installer modes" + }, + "InstallerSwitches": { + "type": "object", + "properties": { + "Silent": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Silent is the value that should be passed to the installer when user chooses a silent or quiet install" + }, + "SilentWithProgress": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "SilentWithProgress is the value that should be passed to the installer when user chooses a non-interactive install" + }, + "Interactive": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Interactive is the value that should be passed to the installer when user chooses an interactive install" + }, + "InstallLocation": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "InstallLocation is the value passed to the installer for custom install location. <INSTALLPATH> token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Log": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Log is the value passed to the installer for custom log file path. <LOGPATH> token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Upgrade": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Upgrade is the value that should be passed to the installer when user chooses an upgrade" + }, + "Custom": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 2048, + "description": "Custom switches will be passed directly to the installer by winget" + } + } + }, + "InstallerSuccessCodes": { + "type": [ "array", "null" ], + "items": { + "type": "integer", + "not": { + "enum": [ 0 ] + } + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of additional non-zero installer success exit codes other than known default values by winget" + }, + "UpgradeBehavior": { + "type": [ "string", "null" ], + "enum": [ + "install", + "uninstallPrevious" + ], + "description": "The upgrade method" + }, + "Commands": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of commands or aliases to run the package" + }, + "Protocols": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "pattern": "^[a-z][-a-z0-9\\.\\+]*$", + "maxLength": 2048 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of protocols the package provides a handler for" + }, + "FileExtensions": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 64 + }, + "maxItems": 256, + "uniqueItems": true, + "description": "List of file extensions the package could support" + }, + "Dependencies": { + "type": [ "object", "null" ], + "properties": { + "WindowsFeatures": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of Windows feature dependencies" + }, + "WindowsLibraries": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of Windows library dependencies" + }, + "PackageDependencies": { + "type": [ "array", "null" ], + "items": { + "type": "object", + "properties": { + "PackageIdentifier": { + "$ref": "#/definitions/PackageIdentifier" + }, + "MinimumVersion": { + "$ref": "#/definitions/PackageVersion" + } + }, + "required": [ "PackageIdentifier" ] + }, + "maxItems": 16, + "description": "List of package dependencies from current source" + }, + "ExternalDependencies": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of external package dependencies" + } + } + }, + "PackageFamilyName": { + "type": [ "string", "null" ], + "pattern": "^[A-Za-z0-9][-\\.A-Za-z0-9]+_[A-Za-z0-9]{13}$", + "maxLength": 255, + "description": "PackageFamilyName for appx or msix installer. Could be used for correlation of packages across sources" + }, + "ProductCode": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 255, + "description": "ProductCode could be used for correlation of packages across sources" + }, + "Capabilities": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 1000, + "uniqueItems": true, + "description": "List of appx or msix installer capabilities" + }, + "RestrictedCapabilities": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 1000, + "uniqueItems": true, + "description": "List of appx or msix installer restricted capabilities" + }, + "Installer": { + "type": "object", + "properties": { + "InstallerLocale": { + "$ref": "#/definitions/Locale" + }, + "Platform": { + "$ref": "#/definitions/Platform" + }, + "MinimumOSVersion": { + "$ref": "#/definitions/MinimumOSVersion" + }, + "Architecture": { + "type": "string", + "enum": [ + "x86", + "x64", + "arm", + "arm64", + "neutral" + ], + "description": "The installer target architecture" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "Scope": { + "$ref": "#/definitions/Scope" + }, + "InstallerUrl": { + "type": "string", + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://.+$", + "maxLength": 2048, + "description": "The installer Url" + }, + "InstallerSha256": { + "type": "string", + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "Sha256 is required. Sha256 of the installer" + }, + "SignatureSha256": { + "type": [ "string", "null" ], + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "SignatureSha256 is recommended for appx or msix. It is the sha256 of signature file inside appx or msix. Could be used during streaming install if applicable" + }, + "InstallModes": { + "$ref": "#/definitions/InstallModes" + }, + "InstallerSwitches": { + "$ref": "#/definitions/InstallerSwitches" + }, + "InstallerSuccessCodes": { + "$ref": "#/definitions/InstallerSuccessCodes" + }, + "UpgradeBehavior": { + "$ref": "#/definitions/UpgradeBehavior" + }, + "Commands": { + "$ref": "#/definitions/Commands" + }, + "Protocols": { + "$ref": "#/definitions/Protocols" + }, + "FileExtensions": { + "$ref": "#/definitions/FileExtensions" + }, + "Dependencies": { + "$ref": "#/definitions/Dependencies" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Capabilities": { + "$ref": "#/definitions/Capabilities" + }, + "RestrictedCapabilities": { + "$ref": "#/definitions/RestrictedCapabilities" + } + }, + "required": [ + "Architecture", + "InstallerUrl", + "InstallerSha256" + ] + } + }, + "type": "object", + "properties": { + "PackageIdentifier": { + "$ref": "#/definitions/PackageIdentifier" + }, + "PackageVersion": { + "$ref": "#/definitions/PackageVersion" + }, + "PackageLocale": { + "type": "string", + "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", + "maxLength": 20, + "description": "The package meta-data locale" + }, + "Publisher": { + "type": "string", + "minLength": 2, + "maxLength": 256, + "description": "The publisher name" + }, + "PublisherUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher home page" + }, + "PublisherSupportUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher support page" + }, + "PrivacyUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher privacy page or the package privacy page" + }, + "Author": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 256, + "description": "The package author" + }, + "PackageName": { + "type": "string", + "minLength": 2, + "maxLength": 256, + "description": "The package name" + }, + "PackageUrl": { + "$ref": "#/definitions/Url", + "description": "The package home page" + }, + "License": { + "type": "string", + "minLength": 3, + "maxLength": 512, + "description": "The package license" + }, + "LicenseUrl": { + "$ref": "#/definitions/Url", + "description": "The license page" + }, + "Copyright": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 512, + "description": "The package copyright" + }, + "CopyrightUrl": { + "$ref": "#/definitions/Url", + "description": "The package copyright page" + }, + "ShortDescription": { + "type": "string", + "minLength": 3, + "maxLength": 256, + "description": "The short package description" + }, + "Description": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 10000, + "description": "The full package description" + }, + "Moniker": { + "$ref": "#/definitions/Tag", + "description": "The most common package term" + }, + "Tags": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/Tag" + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of additional package search terms" + }, + "Agreements": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/Agreement" + }, + "maxItems": 128 + }, + "Channel": { + "$ref": "#/definitions/Channel" + }, + "InstallerLocale": { + "$ref": "#/definitions/Locale" + }, + "Platform": { + "$ref": "#/definitions/Platform" + }, + "MinimumOSVersion": { + "$ref": "#/definitions/MinimumOSVersion" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "Scope": { + "$ref": "#/definitions/Scope" + }, + "InstallModes": { + "$ref": "#/definitions/InstallModes" + }, + "InstallerSwitches": { + "$ref": "#/definitions/InstallerSwitches" + }, + "InstallerSuccessCodes": { + "$ref": "#/definitions/InstallerSuccessCodes" + }, + "UpgradeBehavior": { + "$ref": "#/definitions/UpgradeBehavior" + }, + "Commands": { + "$ref": "#/definitions/Commands" + }, + "Protocols": { + "$ref": "#/definitions/Protocols" + }, + "FileExtensions": { + "$ref": "#/definitions/FileExtensions" + }, + "Dependencies": { + "$ref": "#/definitions/Dependencies" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Capabilities": { + "$ref": "#/definitions/Capabilities" + }, + "RestrictedCapabilities": { + "$ref": "#/definitions/RestrictedCapabilities" + }, + "Installers": { + "type": "array", + "items": { + "$ref": "#/definitions/Installer" + }, + "minItems": 1, + "maxItems": 1 + }, + "ManifestType": { + "type": "string", + "default": "singleton", + "const": "singleton", + "description": "The manifest type" + }, + "ManifestVersion": { + "type": "string", + "default": "1.1.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + } + }, + "required": [ + "PackageIdentifier", + "PackageVersion", + "PackageLocale", + "Publisher", + "PackageName", + "License", + "ShortDescription", + "Installers", + "ManifestType", + "ManifestVersion" + ] +}+ \ No newline at end of file diff --git a/schemas/JSON/manifests/v1.1.0/manifest.version.1.1.0.json b/schemas/JSON/manifests/v1.1.0/manifest.version.1.1.0.json @@ -0,0 +1,46 @@ +{ + "$id": "https://aka.ms/winget-manifest.version.1.1.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A representation of a multi-file manifest representing an app version in the OWC. v1.1.0", + "type": "object", + "properties": { + "PackageIdentifier": { + "type": "string", + "pattern": "^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,3}$", + "maxLength": 128, + "description": "The package unique identifier" + }, + "PackageVersion": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 128, + "description": "The package version" + }, + "DefaultLocale": { + "type": "string", + "default": "en-US", + "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", + "maxLength": 20, + "description": "The default package meta-data locale" + }, + "ManifestType": { + "type": "string", + "default": "version", + "const": "version", + "description": "The manifest type" + }, + "ManifestVersion": { + "type": "string", + "default": "1.1.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + } + }, + "required": [ + "PackageIdentifier", + "PackageVersion", + "DefaultLocale", + "ManifestType", + "ManifestVersion" + ] +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -53,6 +53,8 @@ namespace AppInstaller::CLI return Argument{ "location", 'l', Args::Type::InstallLocation, Resource::String::LocationArgumentDescription, ArgumentType::Standard }; case Args::Type::HashOverride: return Argument{ "force", Argument::NoAlias, Args::Type::HashOverride, Resource::String::InstallForceArgumentDescription, ArgumentType::Flag, Settings::TogglePolicy::Policy::HashOverride }; + case Args::Type::AcceptPackageAgreements: + return Argument{ "accept-package-agreements", Argument::NoAlias, Args::Type::AcceptPackageAgreements, Resource::String::AcceptPackageAgreementsArgumentDescription, ArgumentType::Flag }; case Args::Type::HashFile: return Argument{ "file", 'f', Args::Type::HashFile, Resource::String::FileArgumentDescription, ArgumentType::Positional, true }; case Args::Type::Msix: diff --git a/src/AppInstallerCLICore/Commands/COMInstallCommand.cpp b/src/AppInstallerCLICore/Commands/COMInstallCommand.cpp @@ -17,6 +17,8 @@ namespace AppInstaller::CLI { context << Workflow::ReportExecutionStage(ExecutionStage::Discovery) << - Workflow::InstallPackageVersion; + Workflow::SelectInstaller << + Workflow::EnsureApplicableInstaller << + Workflow::InstallSinglePackage; } } diff --git a/src/AppInstallerCLICore/Commands/CompleteCommand.cpp b/src/AppInstallerCLICore/Commands/CompleteCommand.cpp @@ -31,7 +31,6 @@ namespace AppInstaller::CLI std::string CompleteCommand::HelpLink() const { - // TODO: Define me and point to the right location return "https://aka.ms/winget-command-complete"; } diff --git a/src/AppInstallerCLICore/Commands/ExportCommand.cpp b/src/AppInstallerCLICore/Commands/ExportCommand.cpp @@ -47,7 +47,6 @@ namespace AppInstaller::CLI std::string ExportCommand::HelpLink() const { - // TODO: point to correct location return "https://aka.ms/winget-command-export"; } diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -4,7 +4,6 @@ #include "ImportCommand.h" #include "Workflows/CompletionFlow.h" #include "Workflows/ImportExportFlow.h" -#include "Workflows/InstallFlow.h" #include "Workflows/WorkflowBase.h" #include "Resources.h" @@ -18,6 +17,7 @@ namespace AppInstaller::CLI Argument{ "import-file", 'i', Execution::Args::Type::ImportFile, Resource::String::ImportFileArgumentDescription, ArgumentType::Positional, true }, Argument{ "ignore-unavailable", Argument::NoAlias, Execution::Args::Type::IgnoreUnavailable, Resource::String::ImportIgnoreUnavailableArgumentDescription, ArgumentType::Flag }, Argument{ "ignore-versions", Argument::NoAlias, Execution::Args::Type::IgnoreVersions, Resource::String::ImportIgnorePackageVersionsArgumentDescription, ArgumentType::Flag }, + Argument::ForType(Execution::Args::Type::AcceptPackageAgreements), }; } @@ -33,7 +33,6 @@ namespace AppInstaller::CLI std::string ImportCommand::HelpLink() const { - // TODO: point to correct location return "https://aka.ms/winget-command-import"; } @@ -47,6 +46,6 @@ namespace AppInstaller::CLI Workflow::OpenPredefinedSource(Repository::PredefinedSource::Installed) << Workflow::SearchPackagesForImport << Workflow::ReportExecutionStage(Workflow::ExecutionStage::Execution) << - Workflow::InstallMultiple; + Workflow::InstallImportedPackages; } } diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -39,6 +39,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Override), Argument::ForType(Args::Type::InstallLocation), Argument::ForType(Args::Type::HashOverride), + Argument::ForType(Args::Type::AcceptPackageAgreements), }; } @@ -122,6 +123,8 @@ namespace AppInstaller::CLI context << Workflow::ReportExecutionStage(ExecutionStage::Discovery) << Workflow::GetManifest << - Workflow::InstallPackageVersion; + Workflow::SelectInstaller << + Workflow::EnsureApplicableInstaller << + Workflow::InstallSinglePackage; } } diff --git a/src/AppInstallerCLICore/Commands/RootCommand.cpp b/src/AppInstallerCLICore/Commands/RootCommand.cpp @@ -188,6 +188,7 @@ namespace AppInstaller::CLI links.OutputLine({ Resource::LocString(Resource::String::LicenseAgreement).get(), "https://aka.ms/winget-license" }); links.OutputLine({ Resource::LocString(Resource::String::ThirdPartSoftwareNotices).get(), "https://aka.ms/winget-3rdPartyNotice" }); links.OutputLine({ Resource::LocString(Resource::String::MainHomepage).get(), "https://aka.ms/winget" }); + links.OutputLine({ Resource::LocString(Resource::String::WindowsStoreTerms).get(), "https://www.microsoft.com/en-us/storedocs/terms-of-sale" }); links.Complete(); diff --git a/src/AppInstallerCLICore/Commands/UninstallCommand.cpp b/src/AppInstallerCLICore/Commands/UninstallCommand.cpp @@ -16,7 +16,6 @@ namespace AppInstaller::CLI { std::vector<Argument> UninstallCommand::GetArguments() const { - // TODO: determine exact arguments needed return { Argument::ForType(Args::Type::Query), @@ -79,7 +78,6 @@ namespace AppInstaller::CLI std::string UninstallCommand::HelpLink() const { - // TODO: point to correct location return "https://aka.ms/winget-command-uninstall"; } diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -42,6 +42,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Override), Argument::ForType(Args::Type::InstallLocation), Argument::ForType(Args::Type::HashOverride), + Argument::ForType(Args::Type::AcceptPackageAgreements), Argument{ "all", Argument::NoAlias, Args::Type::All, Resource::String::UpdateAllArgumentDescription, ArgumentType::Flag }, }; } @@ -93,7 +94,6 @@ namespace AppInstaller::CLI std::string UpgradeCommand::HelpLink() const { - // TODO: point to correct location return "https://aka.ms/winget-command-upgrade"; } @@ -149,11 +149,8 @@ namespace AppInstaller::CLI GetInstalledPackageVersion << EnsureUpdateVersionApplicable << SelectInstaller << - EnsureApplicableInstaller << - ReportIdentityAndInstallationDisclaimer << - GetDependenciesFromInstaller << - ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << - InstallPackageInstaller; + EnsureApplicableInstaller << + InstallSinglePackage; } else { @@ -179,11 +176,7 @@ namespace AppInstaller::CLI context << SelectLatestApplicableUpdate(true); } - context << - ReportIdentityAndInstallationDisclaimer << - GetDependenciesFromInstaller << - ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << - InstallPackageInstaller; + context << InstallSinglePackage; } } } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -39,6 +39,7 @@ namespace AppInstaller::CLI::Execution InstallLocation, InstallScope, HashOverride, // Ignore hash mismatches + AcceptPackageAgreements, // Accept all license agreements for packages //Source Command SourceName, diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h @@ -42,7 +42,7 @@ namespace AppInstaller::CLI::Execution // On export: A collection of packages to be exported to a file // On import: A collection of packages read from a file PackageCollection, - // On import: A collection of specific package versions to install + // On import and upgrade all: A collection of specific package versions to install PackagesToInstall, // On import: Sources for the imported packages Sources, @@ -51,10 +51,30 @@ namespace AppInstaller::CLI::Execution Max }; + // Contains all the information needed to install a package. + // This is used when installing multiple packages to pass all the + // data to a sub-context. struct PackageToInstall { + PackageToInstall( + std::shared_ptr<Repository::IPackageVersion>&& packageVersion, + std::shared_ptr<Repository::IPackageVersion>&& installedPackageVersion, + Manifest::Manifest&& manifest, + Manifest::ManifestInstaller&& installer, + Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown) + : PackageVersion(std::move(packageVersion)), InstalledPackageVersion(std::move(installedPackageVersion)), Manifest(std::move(manifest)), Installer(std::move(installer)), Scope(scope) { } + std::shared_ptr<Repository::IPackageVersion> PackageVersion; - PackageCollection::Package PackageRequest; + + // Used to uninstall the old version if needed. + std::shared_ptr<Repository::IPackageVersion> InstalledPackageVersion; + + // Use this instead of the PackageVersion->GetManifest() as the locale was + // applied when selecting the installer. + Manifest::Manifest Manifest; + + Manifest::ManifestInstaller Installer; + Manifest::ScopeEnum Scope = Manifest::ScopeEnum::Unknown; }; namespace details diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -15,6 +15,7 @@ namespace AppInstaller::CLI::Execution const Sequence& NameEmphasis = TextFormat::Foreground::BrightCyan; const Sequence& IdEmphasis = TextFormat::Foreground::BrightCyan; const Sequence& UrlEmphasis = TextFormat::Foreground::BrightBlue; + const Sequence& PromptEmphasis = TextFormat::Foreground::Bright; Reporter::Reporter(std::ostream& outStream, std::istream& inStream) : m_out(outStream), @@ -100,6 +101,62 @@ namespace AppInstaller::CLI::Execution } } + bool Reporter::PromptForBoolResponse(Resource::LocString message, Level level) + { + bool defaultResponse = false; + const std::vector<BoolPromptOption> options + { + BoolPromptOption{ Resource::String::PromptOptionYes, 'Y', true }, + BoolPromptOption{ Resource::String::PromptOptionNo, 'N', false }, + }; + + auto out = GetOutputStream(level); + out << message << std::endl; + + // Try prompting until we get a recognized option + for (;;) + { + // Output all options + for (size_t i = 0; i < options.size(); ++i) + { + out << PromptEmphasis << "[" + options[i].Hotkey.get() + "] " + options[i].Label.get(); + + if (i + 1 == options.size()) + { + out << PromptEmphasis << ": "; + } + else + { + out << " "; + } + } + + // Read the response + std::string response; + if (!std::getline(m_in, response)) + { + THROW_HR(APPINSTALLER_CLI_ERROR_PROMPT_INPUT_ERROR); + } + + // If response was empty, use the default + if (Utility::IsEmptyOrWhitespace(response)) + { + return defaultResponse; + } + + // Find the matching option ignoring whitespace + Utility::Trim(response); + for (const auto& option : options) + { + if (Utility::CaseInsensitiveEquals(response, option.Label) || + Utility::CaseInsensitiveEquals(response, option.Hotkey)) + { + return option.Value; + } + } + } + } + void Reporter::ShowIndefiniteProgress(bool running) { if (m_spinner) diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h @@ -22,6 +22,19 @@ namespace AppInstaller::CLI::Execution { #define WINGET_OSTREAM_FORMAT_HRESULT(hr) "0x" << Logging::SetHRFormat << hr + // One of the options available to the users when prompting for something. + struct BoolPromptOption + { + BoolPromptOption(Resource::StringId labelId, char hotkey, bool value) + : Label(labelId), Hotkey(std::string(1, hotkey)), Value(value) {} + + Utility::LocIndString Hotkey; + Resource::LocString Label; + + // Value associated with this option. + bool Value; + }; + // Reporter should be the central place to show workflow status to user. struct Reporter : public IProgressSink { @@ -82,6 +95,9 @@ namespace AppInstaller::CLI::Execution // Sets the visual style (mostly for progress currently) void SetStyle(AppInstaller::Settings::VisualStyle style); + // Prompts the user, return true if they consented. + bool PromptForBoolResponse(Resource::LocString message, Level level = Level::Info); + // Used to show indefinite progress. Currently an indefinite spinner is the form of // showing indefinite progress. // running: shows indefinite progress if set to true, stops indefinite progress if set to false @@ -146,4 +162,5 @@ namespace AppInstaller::CLI::Execution extern const VirtualTerminal::Sequence& NameEmphasis; extern const VirtualTerminal::Sequence& IdEmphasis; extern const VirtualTerminal::Sequence& UrlEmphasis; + extern const VirtualTerminal::Sequence& PromptEmphasis; } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -19,6 +19,7 @@ namespace AppInstaller::CLI::Resource // struct String { + WINGET_DEFINE_RESOURCE_STRINGID(AcceptPackageAgreementsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(AdjoinedNotFlagError); WINGET_DEFINE_RESOURCE_STRINGID(AdjoinedNotFoundError); WINGET_DEFINE_RESOURCE_STRINGID(AvailableArguments); @@ -44,6 +45,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExportCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExportCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ExportedPackageRequiresLicenseAgreement); WINGET_DEFINE_RESOURCE_STRINGID(ExportIncludeVersionsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExportSourceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExternalDependencies); @@ -108,6 +110,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InvalidJsonFile); WINGET_DEFINE_RESOURCE_STRINGID(InvalidNameError); WINGET_DEFINE_RESOURCE_STRINGID(LicenseAgreement); + WINGET_DEFINE_RESOURCE_STRINGID(LicenseAgreementPrompt); + WINGET_DEFINE_RESOURCE_STRINGID(LicenseNotAgreedTo); WINGET_DEFINE_RESOURCE_STRINGID(Links); WINGET_DEFINE_RESOURCE_STRINGID(ListCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ListCommandShortDescription); @@ -158,6 +162,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PoliciesState); WINGET_DEFINE_RESOURCE_STRINGID(PositionArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PrivacyStatement); + WINGET_DEFINE_RESOURCE_STRINGID(PromptOptionNo); + WINGET_DEFINE_RESOURCE_STRINGID(PromptOptionYes); WINGET_DEFINE_RESOURCE_STRINGID(QueryArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(RainbowArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ReportIdentityFound); @@ -180,6 +186,21 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ShowChannel); WINGET_DEFINE_RESOURCE_STRINGID(ShowCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ShowCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelAgreements); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelAuthor); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstaller); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerLocale); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerProductId); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerSha256); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerType); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerUrl); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelLicense); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelLicenseUrl); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelMoniker); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelPackageUrl); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelPublisher); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelVersion); WINGET_DEFINE_RESOURCE_STRINGID(ShowVersion); WINGET_DEFINE_RESOURCE_STRINGID(SilentArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SingleCharAfterDashError); @@ -263,6 +284,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(VersionsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(WindowsFeaturesDependencies); WINGET_DEFINE_RESOURCE_STRINGID(WindowsLibrariesDependencies); + WINGET_DEFINE_RESOURCE_STRINGID(WindowsStoreTerms); WINGET_DEFINE_RESOURCE_STRINGID(WordArgumentDescription); }; diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -119,6 +119,13 @@ namespace AppInstaller::CLI::Workflow AICLI_LOG(CLI, Info, << "Installed package is available. Package Id [" << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << "], Source [" << sourceDetails.Identifier << "]"); + if (!availablePackageVersion->GetManifest().DefaultLocalization.Get<Manifest::Localization::Agreements>().empty()) + { + // Report that the package requires accepting license terms + AICLI_LOG(CLI, Warning, << "Package [" << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << "] requires license agreement to install"); + context.Reporter.Warn() << Resource::String::ExportedPackageRequiresLicenseAgreement << ' ' << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << std::endl; + } + // Find the exported source for this package auto sourceItr = FindSource(exportedSources, sourceDetails); if (sourceItr == exportedSources.end()) @@ -271,11 +278,16 @@ namespace AppInstaller::CLI::Workflow searchContext.Add<Execution::Data::Source>(source); searchContext.Add<Execution::Data::SearchResult>(source->Search(searchRequest)); + // TODO: In the future, it would be better to not have to convert back and forth from a string + searchContext.Args.AddArg(Execution::Args::Type::InstallScope, ScopeToString(packageRequest.Scope)); + // Find the single version we want is available searchContext << Workflow::EnsureOneMatchFromSearchResult(false) << Workflow::GetManifestWithVersionFromPackage(packageRequest.VersionAndChannel) << - Workflow::GetInstalledPackageVersion; + Workflow::GetInstalledPackageVersion << + Workflow::SelectInstaller << + Workflow::EnsureApplicableInstaller; if (searchContext.Contains(Execution::Data::InstalledPackageVersion) && searchContext.Get<Execution::Data::InstalledPackageVersion>()) { @@ -307,7 +319,12 @@ namespace AppInstaller::CLI::Workflow } } - packagesToInstall.push_back({ std::move(searchContext.Get<Execution::Data::PackageVersion>()), packageRequest }); + packagesToInstall.emplace_back( + std::move(searchContext.Get<Execution::Data::PackageVersion>()), + std::move(searchContext.Get<Execution::Data::InstalledPackageVersion>()), + std::move(searchContext.Get<Execution::Data::Manifest>()), + std::move(searchContext.Get<Execution::Data::Installer>().value()), + packageRequest.Scope); } } @@ -326,4 +343,14 @@ namespace AppInstaller::CLI::Workflow context.Add<Execution::Data::PackagesToInstall>(std::move(packagesToInstall)); } + + void InstallImportedPackages(Execution::Context& context) + { + context << Workflow::InstallMultiplePackages(Resource::String::ImportCommandReportDependencies, APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED); + + if (context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED) + { + context.Reporter.Error() << Resource::String::ImportInstallFailed << std::endl; + } + } } diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.h b/src/AppInstallerCLICore/Workflows/ImportExportFlow.h @@ -35,4 +35,10 @@ namespace AppInstaller::CLI::Workflow // Inputs: PackageCollection, Sources, Source // Outputs: PackagesToInstall void SearchPackagesForImport(Execution::Context& context); + + // Installs all the packages found in the import file. + // Required Args: None + // Inputs: PackagesToInstall + // Outputs: None + void InstallImportedPackages(Execution::Context& context); } diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "InstallFlow.h" #include "UninstallFlow.h" +#include "ShowFlow.h" #include "Resources.h" #include "ShellExecuteInstallerHandler.h" #include "MSStoreInstallerHandler.h" @@ -65,6 +66,82 @@ namespace AppInstaller::CLI::Workflow } } + void ShowLicenseAgreements::operator()(Execution::Context& context) const + { + const auto& manifest = context.Get<Execution::Data::Manifest>(); + auto agreements = manifest.CurrentLocalization.Get<AppInstaller::Manifest::Localization::Agreements>(); + + if (agreements.empty()) + { + // Nothing to do + return; + } + + context << Workflow::ShowPackageInfo; + context.Reporter.Info() << std::endl; + + if (m_ensureAcceptance) + { + context << Workflow::EnsureLicenseAcceptance(/* showPrompt */ true); + } + } + + void EnsureLicenseAcceptance::operator()(Execution::Context& context) const + { + if (context.Args.Contains(Execution::Args::Type::AcceptPackageAgreements)) + { + AICLI_LOG(CLI, Info, << "License agreements accepted by CLI flag"); + return; + } + + if (m_showPrompt) + { + bool accepted = context.Reporter.PromptForBoolResponse(Resource::String::LicenseAgreementPrompt); + if (accepted) + { + AICLI_LOG(CLI, Info, << "License agreements accepted in prompt"); + return; + } + else + { + AICLI_LOG(CLI, Info, << "License agreements not accepted in prompt"); + } + } + + AICLI_LOG(CLI, Error, << "License not agreed to."); + context.Reporter.Error() << Resource::String::LicenseNotAgreedTo << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_LICENSE_NOT_ACCEPTED); + } + + void EnsureLicenseAcceptanceForMultipleInstallers(Execution::Context& context) + { + bool hasLicenseAgreements = false; + for (auto package : context.Get<Execution::Data::PackagesToInstall>()) + { + // Show agreements for each package in a sub-context + auto showContextPtr = context.Clone(); + Execution::Context& showContext = *showContextPtr; + + showContext.Add<Execution::Data::Manifest>(package.Manifest); + + showContext << + Workflow::ReportManifestIdentity << + Workflow::ShowLicenseAgreements(/* ensureAcceptance */ false); + if (showContext.IsTerminated()) + { + AICLI_TERMINATE_CONTEXT(showContext.GetTerminationHR()); + } + + hasLicenseAgreements |= !package.Manifest.CurrentLocalization.Get<AppInstaller::Manifest::Localization::Agreements>().empty(); + } + + // If any package has agreements, ensure they are accepted + if (hasLicenseAgreements) + { + context << Workflow::EnsureLicenseAcceptance(/* showPrompt */ false); + } + } + void DownloadInstaller(Execution::Context& context) { const auto& installer = context.Get<Execution::Data::Installer>().value(); @@ -409,90 +486,63 @@ namespace AppInstaller::CLI::Workflow Workflow::RemoveInstaller; } - void InstallPackageVersion(Execution::Context& context) + void InstallSinglePackage(Execution::Context& context) { context << - Workflow::SelectInstaller << - Workflow::EnsureApplicableInstaller << Workflow::ReportIdentityAndInstallationDisclaimer << + Workflow::ShowLicenseAgreements(/* ensureAcceptance */ true) << Workflow::GetDependenciesFromInstaller << Workflow::ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << Workflow::InstallPackageInstaller; } - const struct PackagesAndInstallers + void InstallMultiplePackages::operator()(Execution::Context& context) const { - PackagesAndInstallers(std::optional<AppInstaller::Manifest::ManifestInstaller> inst, - AppInstaller::CLI::Execution::PackageToInstall pkg) : Installer(inst), Package(pkg) {} - - std::optional<AppInstaller::Manifest::ManifestInstaller> Installer; - AppInstaller::CLI::Execution::PackageToInstall Package; - }; + // Show all license agreements before installing anything + context << Workflow::EnsureLicenseAcceptanceForMultipleInstallers; + if (context.IsTerminated()) + { + return; + } - void InstallMultiple(Execution::Context& context) - { - bool allSucceeded = true; + // Report dependencies DependencyList allDependencies; - std::vector<PackagesAndInstallers> installers; - for (auto package : context.Get<Execution::Data::PackagesToInstall>()) { - Logging::SubExecutionTelemetryScope subExecution; - - // We want to do best effort to install all packages regardless of previous failures - auto installContextPtr = context.Clone(); - Execution::Context& installContext = *installContextPtr; - - // Extract the data needed for installing - installContext.Add<Execution::Data::PackageVersion>(package.PackageVersion); - installContext.Add<Execution::Data::Manifest>(package.PackageVersion->GetManifest()); - - // TODO: In the future, it would be better to not have to convert back and forth from a string - installContext.Args.AddArg(Execution::Args::Type::InstallScope, ScopeToString(package.PackageRequest.Scope)); - - installContext << - Workflow::SelectInstaller << - Workflow::EnsureApplicableInstaller; - - if (installContext.IsTerminated()) - { - allSucceeded = false; - continue; - } - - const auto& installer = installContext.Get<Execution::Data::Installer>(); - installers.push_back(PackagesAndInstallers(installer, package)); - if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) { - if (installer) allDependencies.Add(installer->Dependencies); + allDependencies.Add(package.Installer.Dependencies); } } if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) { context.Add<Execution::Data::Dependencies>(allDependencies); - context << Workflow::ReportDependencies(Resource::String::ImportCommandReportDependencies); + context << Workflow::ReportDependencies(m_dependenciesReportMessage); } - for (auto packageAndInstaller : installers) + bool allSucceeded = true; + for (auto package : context.Get<Execution::Data::PackagesToInstall>()) { - auto package = packageAndInstaller.Package; - auto installer = packageAndInstaller.Installer; + Logging::SubExecutionTelemetryScope subExecution; + // We want to do best effort to install all packages regardless of previous failures auto installContextPtr = context.Clone(); Execution::Context& installContext = *installContextPtr; - // set data needed for installing + // Extract the data needed for installing installContext.Add<Execution::Data::PackageVersion>(package.PackageVersion); installContext.Add<Execution::Data::Manifest>(package.PackageVersion->GetManifest()); - installContext.Args.AddArg(Execution::Args::Type::InstallScope, ScopeToString(package.PackageRequest.Scope)); - installContext.Add<Execution::Data::Installer>(installer); + installContext.Add<Execution::Data::InstalledPackageVersion>(package.InstalledPackageVersion); + installContext.Add<Execution::Data::Installer>(package.Installer); installContext << - ReportIdentityAndInstallationDisclaimer << + Workflow::ReportManifestIdentity << + Workflow::ShowInstallationDisclaimer << Workflow::InstallPackageInstaller; + installContext.Reporter.Info() << std::endl; + if (installContext.IsTerminated()) { if (context.IsTerminated() && context.GetTerminationHR() == E_ABORT) @@ -502,14 +552,16 @@ namespace AppInstaller::CLI::Workflow return; } - allSucceeded = false; + if (m_ignorableInstallResults.end() == std::find(m_ignorableInstallResults.begin(), m_ignorableInstallResults.end(), installContext.GetTerminationHR())) + { + allSucceeded = false; + } } } if (!allSucceeded) { - context.Reporter.Error() << Resource::String::ImportInstallFailed << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED); + AICLI_TERMINATE_CONTEXT(m_resultOnFailure); } } diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -23,6 +23,43 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void ShowInstallationDisclaimer(Execution::Context& context); + // Shows the license agreements if the application has them. + // Required Args: None + // Inputs: Manifest + // Outputs: None + struct ShowLicenseAgreements : public WorkflowTask + { + ShowLicenseAgreements(bool ensureAcceptance) : WorkflowTask("ShowLicenseAgreements"), m_ensureAcceptance(ensureAcceptance) {} + + void operator()(Execution::Context& context) const override; + + private: + // Whether we need to ensure that the agreements are accepted, or only show them. + bool m_ensureAcceptance; + }; + + // Ensure the user accepted the license agreements. + // Required Args: None + // Inputs: None + // Outputs: None + struct EnsureLicenseAcceptance : public WorkflowTask + { + EnsureLicenseAcceptance(bool showPrompt) : WorkflowTask("EnsureLicenseAcceptance"), m_showPrompt(showPrompt) {} + + void operator()(Execution::Context& context) const override; + + private: + // Whether to show an interactive prompt + bool m_showPrompt; + }; + + // Ensure that the user accepted all the license agreements when there are + // multiple installers. + // Required Args: None + // Inputs: PackagesToInstall + // Outputs: None + void EnsureLicenseAcceptanceForMultipleInstallers(Execution::Context& context); + // Composite flow that chooses what to do based on the installer type. // Required Args: None // Inputs: Manifest, Installer @@ -83,23 +120,38 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void ReportIdentityAndInstallationDisclaimer(Execution::Context& context); - // Installs a specific package installer. + // Installs a specific package installer. See also InstallSinglePackage & InstallMultiplePackages. // Required Args: None - // Inputs: Manifest, Installer + // Inputs: Manifest, Installer, PackageVersion, InstalledPackageVersion? // Outputs: None void InstallPackageInstaller(Execution::Context& context); - // Installs a specific package version. - // Required Args: None - // Inputs: Manifest, PackageVersion, Source + // Installs a single package. This also does the reporting and user interaction + // for single-package installation. + // RequiredArgs: None + // Inputs: Manifest, Installer, PackageVersion, InstalledPackageVersion? // Outputs: None - void InstallPackageVersion(Execution::Context& context); + void InstallSinglePackage(Execution::Context& context); - // Installs multiple packages. + // Installs multiple packages. This also does the reporting and user interaction needed. // Required Args: None - // Inputs: Manifests + // Inputs: PackagesToInstall // Outputs: None - void InstallMultiple(Execution::Context& context); + struct InstallMultiplePackages : public WorkflowTask + { + InstallMultiplePackages(StringResource::StringId dependenciesReportMessage, HRESULT resultOnFailure, std::vector<HRESULT>&& ignorableInstallResults = {}) : + WorkflowTask("InstallMultiplePackages"), + m_dependenciesReportMessage(dependenciesReportMessage), + m_resultOnFailure(resultOnFailure), + m_ignorableInstallResults(std::move(ignorableInstallResults)) {} + + void operator()(Execution::Context& context) const override; + + private: + HRESULT m_resultOnFailure; + std::vector<HRESULT> m_ignorableInstallResults; + StringResource::StringId m_dependenciesReportMessage; + }; // Stores the existing set of packages in ARP. // Required Args: None diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.cpp b/src/AppInstallerCLICore/Workflows/ShowFlow.cpp @@ -7,25 +7,31 @@ #include "TableOutput.h" using namespace AppInstaller::Repository; +using namespace AppInstaller::CLI; namespace AppInstaller::CLI::Workflow { void ShowManifestInfo(Execution::Context& context) { + context << ShowPackageInfo << ShowInstallerInfo; + } + + void ShowPackageInfo(Execution::Context& context) + { const auto& manifest = context.Get<Execution::Data::Manifest>(); - const auto& installer = context.Get<Execution::Data::Installer>(); + auto info = context.Reporter.Info(); // TODO: Come up with a prettier format - context.Reporter.Info() << Execution::ManifestInfoEmphasis << "PackageVersion: " << manifest.Version << std::endl; - context.Reporter.Info() << Execution::ManifestInfoEmphasis << "Publisher: " << manifest.CurrentLocalization.Get<Manifest::Localization::Publisher>() << std::endl; + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelVersion << " " << manifest.Version << std::endl; + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelPublisher << " " << manifest.CurrentLocalization.Get<Manifest::Localization::Publisher>() << std::endl; auto author = manifest.CurrentLocalization.Get<Manifest::Localization::Author>(); if (!author.empty()) { - context.Reporter.Info() << Execution::ManifestInfoEmphasis << "Author: " << author << std::endl; + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelAuthor << " " << author << std::endl; } if (!manifest.Moniker.empty()) { - context.Reporter.Info() << Execution::ManifestInfoEmphasis << "Moniker: " << manifest.Moniker << std::endl; + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelMoniker << " " << manifest.Moniker << std::endl; } auto description = manifest.CurrentLocalization.Get<Manifest::Localization::Description>(); if (description.empty()) @@ -35,43 +41,73 @@ namespace AppInstaller::CLI::Workflow } if (!description.empty()) { - context.Reporter.Info() << Execution::ManifestInfoEmphasis << "Description: " << description << std::endl; + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelDescription << " " << description << std::endl; } auto homepage = manifest.CurrentLocalization.Get<Manifest::Localization::PackageUrl>(); if (!homepage.empty()) { - context.Reporter.Info() << Execution::ManifestInfoEmphasis << "PackageUrl: " << homepage << std::endl; + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelPackageUrl << " " << homepage << std::endl; } - context.Reporter.Info() << Execution::ManifestInfoEmphasis << "License: " << manifest.CurrentLocalization.Get<Manifest::Localization::License>() << std::endl; + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelLicense << " " << manifest.CurrentLocalization.Get<Manifest::Localization::License>() << std::endl; auto licenseUrl = manifest.CurrentLocalization.Get<Manifest::Localization::LicenseUrl>(); if (!licenseUrl.empty()) { - context.Reporter.Info() << Execution::ManifestInfoEmphasis << "LicenseUrl: " << licenseUrl << std::endl; + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelLicenseUrl << " " << licenseUrl << std::endl; + } + auto agreements = manifest.CurrentLocalization.Get<Manifest::Localization::Agreements>(); + if (!agreements.empty()) + { + context.Reporter.Info() << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelAgreements << std::endl; + for (const auto& agreement : agreements) + { + if (!agreement.Label.empty()) + { + info << Execution::ManifestInfoEmphasis << agreement.Label << " "; + } + + if (!agreement.AgreementText.empty()) + { + info << agreement.AgreementText << std::endl; + } + + if (!agreement.AgreementUrl.empty()) + { + info << agreement.AgreementUrl << std::endl; + } + } + + info << std::endl; } + } - context.Reporter.Info() << Execution::ManifestInfoEmphasis << "Installer:" << std::endl; + void ShowInstallerInfo(Execution::Context& context) + { + const auto& installer = context.Get<Execution::Data::Installer>(); + auto info = context.Reporter.Info(); + + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelInstaller << std::endl; if (installer) { - context.Reporter.Info() << Execution::ManifestInfoEmphasis << " InstallerType: " << Manifest::InstallerTypeToString(installer->InstallerType) << std::endl; + info << " " << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelInstallerType << " " << Manifest::InstallerTypeToString(installer->InstallerType) << std::endl; if (!installer->Locale.empty()) { - context.Reporter.Info() << Execution::ManifestInfoEmphasis << " InstallerLocale: " << installer->Locale << std::endl; + info << " " << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelInstallerLocale << " " << installer->Locale << std::endl; } if (!installer->Url.empty()) { - context.Reporter.Info() << Execution::ManifestInfoEmphasis << " InstallerUrl: " << installer->Url << std::endl; + info << " " << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelInstallerUrl << " " << installer->Url << std::endl; } if (!installer->Sha256.empty()) { - context.Reporter.Info() << Execution::ManifestInfoEmphasis << " InstallerSha256: " << Utility::SHA256::ConvertToString(installer->Sha256) << std::endl; + info << " " << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelInstallerSha256 << " " << Utility::SHA256::ConvertToString(installer->Sha256) << std::endl; } if (!installer->ProductId.empty()) { - context.Reporter.Info() << Execution::ManifestInfoEmphasis << " ProductId: " << installer->ProductId << std::endl; + info << " " << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelInstallerProductId << " " << installer->ProductId << std::endl; } - if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) { - auto info = context.Reporter.Info(); + if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) + { const auto& dependencies = installer->Dependencies; if (dependencies.HasAny()) @@ -93,11 +129,12 @@ namespace AppInstaller::CLI::Workflow if (dependencies.HasAnyOf(Manifest::DependencyType::Package)) { info << " - PackageDependencies: " << std::endl; - dependencies.ApplyToType(Manifest::DependencyType::Package, [&info](Manifest::Dependency dependency) { - info << " " << dependency.Id; - if (dependency.MinVersion) info << " [>= " << dependency.MinVersion.value() << "]"; - info << std::endl; - }); + dependencies.ApplyToType(Manifest::DependencyType::Package, [&info](Manifest::Dependency dependency) + { + info << " " << dependency.Id; + if (dependency.MinVersion) info << " [>= " << dependency.MinVersion.value() << "]"; + info << std::endl; + }); } if (dependencies.HasAnyOf(Manifest::DependencyType::External)) @@ -110,7 +147,7 @@ namespace AppInstaller::CLI::Workflow } else { - context.Reporter.Warn() << " No installers are applicable to the current system" << std::endl; + context.Reporter.Warn() << " " << Resource::String::NoApplicableInstallers << std::endl; } } diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.h b/src/AppInstallerCLICore/Workflows/ShowFlow.h @@ -11,6 +11,18 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void ShowManifestInfo(Execution::Context& context); + // Shows information on a package; this is only the information common to all installers. + // Required Args: None + // Inputs: Manifest + // Outputs: None + void ShowPackageInfo(Execution::Context& context); + + // Shows information on an installer + // Required Args: None + // Inputs: Installer + // Outputs: None + void ShowInstallerInfo(Execution::Context& context); + // Shows the version for the specific manifest. // Required Args: None // Inputs: Manifest diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -87,7 +87,7 @@ namespace AppInstaller::CLI::Workflow void UpdateAllApplicable(Execution::Context& context) { const auto& matches = context.Get<Execution::Data::SearchResult>().Matches; - bool updateAllHasFailure = false; + std::vector<Execution::PackageToInstall> packagesToInstall; bool updateAllFoundUpdate = false; for (const auto& match : matches) @@ -112,36 +112,24 @@ namespace AppInstaller::CLI::Workflow updateAllFoundUpdate = true; - updateContext << - ReportIdentityAndInstallationDisclaimer << - GetDependenciesFromInstaller << - ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << - InstallPackageInstaller; - - updateContext.Reporter.Info() << std::endl; - - // msstore update might still terminate with APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE - if (updateContext.GetTerminationHR() != S_OK && - updateContext.GetTerminationHR() != APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE) - { - updateAllHasFailure = true; - } - - if (context.IsTerminated() && context.GetTerminationHR() == E_ABORT) - { - context.Reporter.Info() << Resource::String::Cancelled << std::endl; - return; - } + packagesToInstall.emplace_back( + std::move(updateContext.Get<Execution::Data::PackageVersion>()), + std::move(updateContext.Get<Execution::Data::InstalledPackageVersion>()), + std::move(updateContext.Get<Execution::Data::Manifest>()), + std::move(updateContext.Get<Execution::Data::Installer>().value())); } if (!updateAllFoundUpdate) { context.Reporter.Info() << Resource::String::UpdateNotApplicable << std::endl; + return; } - if (updateAllHasFailure) - { - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_UPDATE_ALL_HAS_FAILURE); - } + context.Add<Execution::Data::PackagesToInstall>(std::move(packagesToInstall)); + context << + InstallMultiplePackages( + Resource::String::InstallAndUpgradeCommandsReportDependencies, + APPINSTALLER_CLI_ERROR_UPDATE_ALL_HAS_FAILURE, + { APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE }); } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -542,7 +542,7 @@ namespace AppInstaller::CLI::Workflow Logging::Telemetry().LogAppFound(package->GetProperty(PackageProperty::Name), package->GetProperty(PackageProperty::Id)); context.Add<Execution::Data::Package>(std::move(package)); - }; + } } void GetManifestWithVersionFromPackage::operator()(Execution::Context& context) const diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net5.0</TargetFramework> + <TargetFramework>net5.0-windows</TargetFramework> <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\AppInstallerCLIE2ETests\</OutDir> <IsPackable>false</IsPackable> <Platforms>x64;x86</Platforms> diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -125,6 +125,14 @@ namespace AppInstallerCLIE2ETests public const int ERROR_UNSUPPORTED_RESTSOURCE = unchecked((int)0x8A150038); public const int ERROR_RESTSOURCE_INVALID_DATA = unchecked((int)0x8A150039); public const int ERROR_BLOCKED_BY_POLICY = unchecked((int)0x8a15003A); + public const int ERROR_RESTSOURCE_INTERNAL_ERROR = unchecked((int)0x8a15003B); + public const int ERROR_RESTSOURCE_INVALID_URL = unchecked((int)0x8a15003C); + public const int ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE = unchecked((int)0x8a15003D); + public const int ERROR_RESTSOURCE_INVALID_VERSION = unchecked((int)0x8a15003E); + public const int ERROR_SOURCE_DATA_INTEGRITY_FAILURE = unchecked((int)0x8a15003F); + public const int ERROR_STREAM_READ_FAILURE = unchecked((int)0x8a150040); + public const int ERROR_LICENSE_NOT_ACCEPTED = unchecked((int)0x8a150041); + public const int ERROR_PROMPT_INPUT_ERROR = unchecked((int)0x8a150042); } } } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -949,4 +949,71 @@ Configuration is disabled due to Group Policy.</value> <data name="WindowsLibrariesDependencies" xml:space="preserve"> <value>Windows Libraries</value> </data> + <data name="WindowsStoreTerms" xml:space="preserve"> + <value>Windows Store Terms</value> + </data> + <data name="AcceptPackageAgreementsArgumentDescription" xml:space="preserve"> + <value>Accept all license agreements for packages</value> + </data> + <data name="ExportedPackageRequiresLicenseAgreement" xml:space="preserve"> + <value>Exported package requires license agreement to install:</value> + </data> + <data name="LicenseAgreementPrompt" xml:space="preserve"> + <value>The publisher requires that you view the following information and accept the EULA before installing. +Do you agree to the terms?</value> + </data> + <data name="LicenseNotAgreedTo" xml:space="preserve"> + <value>License not agreed to. Installation cancelled.</value> + </data> + <data name="ShowLabelAgreements" xml:space="preserve"> + <value>Agreements:</value> + </data> + <data name="ShowLabelAuthor" xml:space="preserve"> + <value>Author:</value> + </data> + <data name="ShowLabelDescription" xml:space="preserve"> + <value>Description:</value> + </data> + <data name="ShowLabelInstaller" xml:space="preserve"> + <value>Installer:</value> + </data> + <data name="ShowLabelInstallerLocale" xml:space="preserve"> + <value>Locale:</value> + </data> + <data name="ShowLabelInstallerProductId" xml:space="preserve"> + <value>Store Product Id:</value> + </data> + <data name="ShowLabelInstallerSha256" xml:space="preserve"> + <value>SHA256:</value> + </data> + <data name="ShowLabelInstallerType" xml:space="preserve"> + <value>Type:</value> + </data> + <data name="ShowLabelInstallerUrl" xml:space="preserve"> + <value>Download Url:</value> + </data> + <data name="ShowLabelLicense" xml:space="preserve"> + <value>License:</value> + </data> + <data name="ShowLabelLicenseUrl" xml:space="preserve"> + <value>License Url:</value> + </data> + <data name="ShowLabelMoniker" xml:space="preserve"> + <value>Moniker:</value> + </data> + <data name="ShowLabelPackageUrl" xml:space="preserve"> + <value>Homepage:</value> + </data> + <data name="ShowLabelPublisher" xml:space="preserve"> + <value>Publisher:</value> + </data> + <data name="ShowLabelVersion" xml:space="preserve"> + <value>Version:</value> + </data> + <data name="PromptOptionNo" xml:space="preserve"> + <value>No</value> + </data> + <data name="PromptOptionYes" xml:space="preserve"> + <value>Yes</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -253,6 +253,9 @@ <CopyFileToFolders Include="TestData\InstallFlowTest_NonZeroExitCode.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_LicenseAgreement.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\ImportFile-Bad-Invalid.json"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> @@ -277,6 +280,12 @@ <CopyFileToFolders Include="TestData\ImportFile-Good-MachineScope.json"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ImportFile-Good-Dependencies.json"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ImportFile-Good-WithLicenseAgreement.json"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <None Include="packages.config" /> <None Include="PropertySheet.props" /> <CopyFileToFolders Include="TestData\InstallerArgTest_Inno_NoSwitches.yaml"> @@ -418,10 +427,10 @@ <CopyFileToFolders Include="TestData\Manifest-Good-InstallerTypeExeRoot-SilentRoot.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> - <CopyFileToFolders Include="TestData\Manifest-Good-Installeruniqueness-DefaultLang.yaml"> + <CopyFileToFolders Include="TestData\Manifest-Good-InstallerUniqueness-DefaultLang.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> - <CopyFileToFolders Include="TestData\Manifest-Good-Installeruniqueness-DiffLangs.yaml"> + <CopyFileToFolders Include="TestData\Manifest-Good-InstallerUniqueness-DiffLangs.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Good-InstallerUniqueness-DiffScope.yaml"> @@ -442,6 +451,9 @@ <CopyFileToFolders Include="TestData\Manifest-Good-Spaces.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Good-AllDependencyTypes.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Encoding-ANSI.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> @@ -475,15 +487,27 @@ <CopyFileToFolders Include="TestData\Manifest-Bad-InvalidUpdateBehavior.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-Channel-NotSupported.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\UpdateFlowTest_Exe.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> <CopyFileToFolders Include="TestData\UpdateFlowTest_Exe_2.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_Exe_2_LicenseAgreement.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_ExeDependencies.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\UpdateFlowTest_Msix.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_Msix_LicenseAgreement.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InputNames.txt"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> @@ -508,24 +532,12 @@ <CopyFileToFolders Include="TestData\MultiFileManifestV1\ManifestV1-MultiFile-Version.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> - <CopyFileToFolders Include="TestData\Manifest-Bad-Channel-NotSupported.yaml"> - <DeploymentContent>true</DeploymentContent> - </CopyFileToFolders> - <CopyFileToFolders Include="TestData\Manifest-Good-AllDependencyTypes.yaml"> - <DeploymentContent>true</DeploymentContent> - </CopyFileToFolders> - <CopyFileToFolders Include="TestData\ImportFile-Good-Dependencies.json"> - <DeploymentContent>true</DeploymentContent> - </CopyFileToFolders> <CopyFileToFolders Include="TestData\Installer_Exe_Dependencies.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> <CopyFileToFolders Include="TestData\Installer_Msi_WFDependency.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> - <CopyFileToFolders Include="TestData\UpdateFlowTest_ExeDependencies.yaml"> - <DeploymentContent>true</DeploymentContent> - </CopyFileToFolders> <CopyFileToFolders Include="TestData\Installer_Exe_DependenciesOnRoot.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -288,10 +288,10 @@ <CopyFileToFolders Include="TestData\Manifest-Good-InstallerTypeExe-SilentRoot.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> - <CopyFileToFolders Include="TestData\Manifest-Good-Installeruniqueness-DefaultLang.yaml"> + <CopyFileToFolders Include="TestData\Manifest-Good-InstallerUniqueness-DefaultLang.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> - <CopyFileToFolders Include="TestData\Manifest-Good-Installeruniqueness-DiffLangs.yaml"> + <CopyFileToFolders Include="TestData\Manifest-Good-InstallerUniqueness-DiffLangs.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Good-InstallerUniqueness-DiffScope.yaml"> @@ -336,6 +336,9 @@ <CopyFileToFolders Include="TestData\InstallFlowTest_Exe.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_LicenseAgreement.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_MSStore.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> @@ -393,15 +396,24 @@ <CopyFileToFolders Include="TestData\UpdateFlowTest_Exe.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_Exe_2_LicenseAgreement.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\UpdateFlowTest_Msix.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_Msix_LicenseAgreement.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\ImportFile-Good.json"> <Filter>TestData</Filter> </CopyFileToFolders> <CopyFileToFolders Include="TestData\ImportFile-Good-AlreadyInstalled.json"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ImportFile-Good-WithLicenseAgreement.json"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\ImportFile-Bad-Malformed.json"> <Filter>TestData</Filter> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Good-WithLicenseAgreement.json b/src/AppInstallerCLITests/TestData/ImportFile-Good-WithLicenseAgreement.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "TestInstallerWithLicenseAgreement", + "Version": "3.0.0.0" + } + ], + "SourceDetails": { + "Argument": "//arg", + "Identifier": "*TestSource", + "Name": "TestSource", + "Type": "Microsoft.TestSource" + } + } + ], + "WinGetVersion": "1.0.0" +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_LicenseAgreement.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_LicenseAgreement.yaml @@ -0,0 +1,22 @@ +PackageIdentifier: TestInstallerWithLicenseAgreement +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Installer +Publisher: Microsoft Corporation +Moniker: AICLITestExe +License: Test +Agreements: + - AgreementLabel: Agreement with text + Agreement: This is the text of the agreement. + - AgreementLabel: Agreement with URL + AgreementUrl: https://TestAgreementUrl +InstallerSwitches: + SilentWithProgress: /silentwithprogress + Silent: /silence +Installers: + - Architecture: x86 + InstallerUrl: https://ThisIsNotUsed + InstallerType: exe + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B +ManifestType: singleton +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/TestData/UpdateFlowTest_Exe_2_LicenseAgreement.yaml b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Exe_2_LicenseAgreement.yaml @@ -0,0 +1,23 @@ +# Similar content to UpdateFlowTest_Exe_2.yaml but with Agreements +PackageIdentifier: AppInstallerCliTest.TestExeInstaller +PackageVersion: 3.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Installer +Publisher: Microsoft Corporation +Moniker: AICLITestExe +License: Test +Agreements: + - AgreementLabel: Agreement for EXE + Agreement: This is the agreement for the EXE installer. +InstallerSwitches: + Custom: /custom /ver3.0.0.0 + SilentWithProgress: /silentwithprogress + Silent: /silence + Update: /update +Installers: + - Architecture: x86 + InstallerUrl: https://ThisIsNotUsed + InstallerType: exe + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B +ManifestType: singleton +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/TestData/UpdateFlowTest_Msix_LicenseAgreement.yaml b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Msix_LicenseAgreement.yaml @@ -0,0 +1,20 @@ +# Similar content to UpdateFlowTest_Msix.yaml but with Agreements +PackageIdentifier: AppInstallerCliTest.TestMsixInstaller +PackageVersion: 2.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test MSIX Installer +Publisher: Microsoft Corporation +Moniker: AICLITestExe +License: Test +Agreements: + - AgreementLabel: Agreement for MSIX + Agreement: This is the agreement for the MSIX installer. +Installers: + - Architecture: X64 + InstallerUrl: https://github.com/microsoft/msix-packaging/blob/master/src/test/testData/unpack/TestAppxPackage_x64.appx?raw=true + InstallerType: msix + InstallerSha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea + SignatureSha256: 138781c3e6f635240353f3d14d1d57bdcb89413e49be63b375e6a5d7b93b0d07 + PackageFamilyName: 20477fca-282d-49fb-b03e-371dca074f0f_8wekyb3d8bbwe +ManifestType: singleton +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -93,6 +93,8 @@ namespace struct WorkflowTestCompositeSource : public TestSource { + WorkflowTestCompositeSource(bool upgradeUsesLicenses) : m_upgradeUsesLicenses(upgradeUsesLicenses) {} + SearchResult Search(const SearchRequest& request) const override { SearchResult result; @@ -113,7 +115,7 @@ namespace { auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe.yaml")); - auto manifest3 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe_2.yaml")); + auto manifest3 = YamlParser::CreateFromPath(TestDataFile(m_upgradeUsesLicenses ? "UpdateFlowTest_Exe_2_LicenseAgreement.yaml" : "UpdateFlowTest_Exe_2.yaml")); result.Matches.emplace_back( ResultMatch( TestPackage::Make( @@ -133,7 +135,7 @@ namespace if (input.empty() || input == "AppInstallerCliTest.TestMsixInstaller") { auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Msix_StreamingFlow.yaml")); - auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Msix.yaml")); + auto manifest2 = YamlParser::CreateFromPath(TestDataFile(m_upgradeUsesLicenses ? "UpdateFlowTest_Msix_LicenseAgreement.yaml" : "UpdateFlowTest_Msix.yaml")); result.Matches.emplace_back( ResultMatch( TestPackage::Make( @@ -233,8 +235,26 @@ namespace PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMsixInstaller.WFDep"))); } + if (input == "TestInstallerWithLicenseAgreement") + { + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_LicenseAgreement.yaml")); + auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe_2_LicenseAgreement.yaml")); + result.Matches.emplace_back( + ResultMatch( + TestPackage::Make( + manifest, + TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Exe" } }, + std::vector<Manifest>{ manifest2, manifest }, + this->shared_from_this() + ), + PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestInstallerWithLicenseAgreement"))); + } + return result; } + + private: + bool m_upgradeUsesLicenses; }; struct TestContext; @@ -286,7 +306,7 @@ namespace { if (!wto.Used) { - FAIL("Unused override"); + FAIL_CHECK("Unused override " + wto.Target.GetName()); } } } @@ -336,15 +356,15 @@ void OverrideForOpenSource(TestContext& context) } }); } -void OverrideForCompositeInstalledSource(TestContext& context) +void OverrideForCompositeInstalledSource(TestContext& context, bool upgradeUsesLicenses = false) { context.Override({ Workflow::OpenSource, [](TestContext&) { } }); - context.Override({ "OpenCompositeSource", [](TestContext& context) + context.Override({ "OpenCompositeSource", [=](TestContext& context) { - context.Add<Execution::Data::Source>(std::make_shared<WorkflowTestCompositeSource>()); + context.Add<Execution::Data::Source>(std::make_shared<WorkflowTestCompositeSource>(upgradeUsesLicenses)); } }); } @@ -357,7 +377,7 @@ void OverrideForImportSource(TestContext& context) context.Override({ Workflow::OpenSourcesForImport, [](TestContext& context) { - context.Add<Execution::Data::Sources>(std::vector<std::shared_ptr<ISource>>{ std::make_shared<WorkflowTestCompositeSource>() }); + context.Add<Execution::Data::Sources>(std::vector<std::shared_ptr<ISource>>{ std::make_shared<WorkflowTestCompositeSource>(false) }); } }); } @@ -779,6 +799,86 @@ TEST_CASE("InstallFlow_SearchFoundMultipleApp", "[InstallFlow][workflow]") REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::MultiplePackagesFound).get()) != std::string::npos); } +TEST_CASE("InstallFlow_LicenseAgreement", "[InstallFlow][workflow]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_LicenseAgreement.yaml").GetPath().u8string()); + context.Args.AddArg(Execution::Args::Type::AcceptPackageAgreements); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify agreements are shown + REQUIRE(installOutput.str().find("Agreement with text") != std::string::npos); + REQUIRE(installOutput.str().find("This is the text of the agreement.") != std::string::npos); + REQUIRE(installOutput.str().find("Agreement with URL") != std::string::npos); + REQUIRE(installOutput.str().find("https://TestAgreementUrl") != std::string::npos); + + // Verify Installer is called. + REQUIRE(std::filesystem::exists(installResultPath.GetPath())); +} + +TEST_CASE("InstallFlow_LicenseAgreement_Prompt", "[InstallFlow][workflow]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + // Accept the agreements by saying "Yes" at the prompt + std::istringstream installInput{ "y" }; + + std::ostringstream installOutput; + TestContext context{ installOutput, installInput }; + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_LicenseAgreement.yaml").GetPath().u8string()); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify prompt was shown + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::LicenseAgreementPrompt).get()) != std::string::npos); + + // Verify agreements are shown + REQUIRE(installOutput.str().find("Agreement with text") != std::string::npos); + REQUIRE(installOutput.str().find("This is the text of the agreement.") != std::string::npos); + REQUIRE(installOutput.str().find("Agreement with URL") != std::string::npos); + REQUIRE(installOutput.str().find("https://TestAgreementUrl") != std::string::npos); + + // Verify Installer is called. + REQUIRE(std::filesystem::exists(installResultPath.GetPath())); +} + +TEST_CASE("InstallFlow_LicenseAgreement_NotAccepted", "[InstallFlow][workflow]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + // Say "No" at the agreements prompt + std::istringstream installInput{ "n" }; + + std::ostringstream installOutput; + TestContext context{ installOutput, installInput }; + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_LicenseAgreement.yaml").GetPath().u8string()); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify agreements are shown + REQUIRE(installOutput.str().find("Agreement with text") != std::string::npos); + REQUIRE(installOutput.str().find("This is the text of the agreement.") != std::string::npos); + REQUIRE(installOutput.str().find("Agreement with URL") != std::string::npos); + REQUIRE(installOutput.str().find("https://TestAgreementUrl") != std::string::npos); + + // Verify installation failed + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_LICENSE_NOT_ACCEPTED); + REQUIRE_FALSE(std::filesystem::exists(installResultPath.GetPath())); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::LicenseNotAgreedTo).get()) != std::string::npos); +} + TEST_CASE("ShowFlow_SearchAndShowAppInfo", "[ShowFlow][workflow]") { std::ostringstream showOutput; @@ -1118,6 +1218,117 @@ TEST_CASE("UpdateFlow_Dependencies", "[UpdateFlow][workflow][dependencies]") REQUIRE(updateResultStr.find("Preview VC Runtime") != std::string::npos); } +TEST_CASE("UpdateFlow_LicenseAgreement", "[UpdateFlow][workflow]") +{ + TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::Query, "TestInstallerWithLicenseAgreement"sv); + context.Args.AddArg(Execution::Args::Type::AcceptPackageAgreements); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify agreements are shown + REQUIRE(updateOutput.str().find("Agreement for EXE") != std::string::npos); + REQUIRE(updateOutput.str().find("This is the agreement for the EXE") != std::string::npos); + + // Verify Installer is called. + REQUIRE(std::filesystem::exists(updateResultPath.GetPath())); +} + +TEST_CASE("UpdateFlow_LicenseAgreement_NotAccepted", "[UpdateFlow][workflow]") +{ + TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); + + // Say "No" at the agreements prompt + std::istringstream updateInput{ "n" }; + + std::ostringstream updateOutput; + TestContext context{ updateOutput, updateInput }; + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::Query, "TestInstallerWithLicenseAgreement"sv); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify agreements are shown + REQUIRE(updateOutput.str().find("Agreement for EXE") != std::string::npos); + REQUIRE(updateOutput.str().find("This is the agreement for the EXE") != std::string::npos); + + // Verify Installer is not called. + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_LICENSE_NOT_ACCEPTED); + REQUIRE_FALSE(std::filesystem::exists(updateResultPath.GetPath())); + REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::LicenseNotAgreedTo).get()) != std::string::npos); +} + +TEST_CASE("UpdateFlow_All_LicenseAgreement", "[UpdateFlow][workflow]") +{ + TestCommon::TempFile updateExeResultPath("TestExeInstalled.txt"); + TestCommon::TempFile updateMsixResultPath("TestMsixInstalled.txt"); + TestCommon::TempFile updateMSStoreResultPath("TestMSStoreUpdated.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + OverrideForCompositeInstalledSource(context, /* upgradeUsesLicenses */ true); + OverrideForShellExecute(context); + OverrideForMSIX(context); + OverrideForMSStore(context, true); + context.Args.AddArg(Execution::Args::Type::All); + context.Args.AddArg(Execution::Args::Type::AcceptPackageAgreements); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify agreements are shown + REQUIRE(updateOutput.str().find("Agreement for EXE") != std::string::npos); + REQUIRE(updateOutput.str().find("This is the agreement for the EXE") != std::string::npos); + REQUIRE(updateOutput.str().find("Agreement for MSIX") != std::string::npos); + REQUIRE(updateOutput.str().find("This is the agreement for the MSIX") != std::string::npos); + + // Verify installers are called. + REQUIRE(std::filesystem::exists(updateExeResultPath.GetPath())); + REQUIRE(std::filesystem::exists(updateMsixResultPath.GetPath())); + REQUIRE(std::filesystem::exists(updateMSStoreResultPath.GetPath())); +} + +TEST_CASE("UpdateFlow_All_LicenseAgreement_NotAccepted", "[UpdateFlow][workflow]") +{ + TestCommon::TempFile updateExeResultPath("TestExeInstalled.txt"); + TestCommon::TempFile updateMsixResultPath("TestMsixInstalled.txt"); + TestCommon::TempFile updateMSStoreResultPath("TestMSStoreUpdated.txt"); + + // Say "No" at the agreements prompt + std::istringstream updateInput{ "n" }; + + std::ostringstream updateOutput; + TestContext context{ updateOutput, updateInput }; + OverrideForCompositeInstalledSource(context, /* upgradeUsesLicenses */ true); + context.Args.AddArg(Execution::Args::Type::All); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify agreements are shown + REQUIRE(updateOutput.str().find("Agreement for EXE") != std::string::npos); + REQUIRE(updateOutput.str().find("This is the agreement for the EXE") != std::string::npos); + REQUIRE(updateOutput.str().find("Agreement for MSIX") != std::string::npos); + REQUIRE(updateOutput.str().find("This is the agreement for the MSIX") != std::string::npos); + + // Verify installers are not called. + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_LICENSE_NOT_ACCEPTED); + REQUIRE_FALSE(std::filesystem::exists(updateExeResultPath.GetPath())); + REQUIRE_FALSE(std::filesystem::exists(updateMsixResultPath.GetPath())); + REQUIRE_FALSE(std::filesystem::exists(updateMSStoreResultPath.GetPath())); +} + TEST_CASE("UninstallFlow_UninstallExe", "[UninstallFlow][workflow]") { TestCommon::TempFile uninstallResultPath("TestExeUninstalled.txt"); @@ -1489,6 +1700,51 @@ TEST_CASE("ImportFlow_Dependencies", "[ImportFlow][workflow][dependencies]") REQUIRE(importOutput.str().find("Hyper-V") != std::string::npos); } +TEST_CASE("ImportFlow_LicenseAgreement", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + OverrideForImportSource(context); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Good-WithLicenseAgreement.json").GetPath().string()); + context.Args.AddArg(Execution::Args::Type::AcceptPackageAgreements); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Verify agreements are shown + REQUIRE(importOutput.str().find("Agreement for EXE") != std::string::npos); + REQUIRE(importOutput.str().find("This is the agreement for the EXE") != std::string::npos); + + // Verify all packages were installed + REQUIRE(std::filesystem::exists(exeInstallResultPath.GetPath())); +} + +TEST_CASE("ImportFlow_LicenseAgreement_NotAccepted", "[ImportFlow][workflow]") +{ + // Say "No" at the agreements prompt + std::istringstream importInput{ "n" }; + + std::ostringstream importOutput; + TestContext context{ importOutput, importInput }; + OverrideForImportSource(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Good-WithLicenseAgreement.json").GetPath().string()); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Verify agreements are shown + REQUIRE(importOutput.str().find("Agreement for EXE") != std::string::npos); + REQUIRE(importOutput.str().find("This is the agreement for the EXE") != std::string::npos); + + // Command should have failed + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_LICENSE_NOT_ACCEPTED); +} + void VerifyMotw(const std::filesystem::path& testFile, DWORD zone) { std::filesystem::path motwFile(testFile); diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -196,8 +196,8 @@ TEST_CASE("ReadGoodManifests", "[ManifestValidation]") { "Manifest-Good-InstallerTypeExeRoot-SilentRoot.yaml" }, { "Manifest-Good-InstallerTypeExe-Silent.yaml" }, { "Manifest-Good-InstallerTypeExe-SilentRoot.yaml" }, - { "Manifest-Good-Installeruniqueness-DefaultLang.yaml" }, - { "Manifest-Good-Installeruniqueness-DiffLangs.yaml" }, + { "Manifest-Good-InstallerUniqueness-DefaultLang.yaml" }, + { "Manifest-Good-InstallerUniqueness-DiffLangs.yaml" }, { "Manifest-Good-InstallerUniqueness-DiffScope.yaml" }, { "Manifest-Good-Minimum.yaml" }, { "Manifest-Good-Minimum-InstallerType.yaml" }, diff --git a/src/AppInstallerCommonCore/AppInstallerStrings.cpp b/src/AppInstallerCommonCore/AppInstallerStrings.cpp @@ -4,7 +4,6 @@ #include "Public/AppInstallerStrings.h" #include "Public/AppInstallerErrors.h" #include "Public/AppInstallerLogging.h" -#include "icu.h" namespace AppInstaller::Utility { diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp @@ -143,6 +143,10 @@ namespace AppInstaller return "The source data is corrupted or tampered"; case APPINSTALLER_CLI_ERROR_STREAM_READ_FAILURE: return "Error reading from the stream"; + case APPINSTALLER_CLI_ERROR_LICENSE_NOT_ACCEPTED: + return "License not agreed to"; + case APPINSTALLER_CLI_ERROR_PROMPT_INPUT_ERROR: + return "Error reading input in prompt"; default: return "Unknown Error Code"; } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp @@ -91,7 +91,30 @@ namespace AppInstaller::Manifest::YamlParser { std::string schemaStr; - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_1 }) + { + switch (manifestType) + { + case AppInstaller::Manifest::ManifestTypeEnum::Singleton: + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_1_SINGLETON), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + break; + case AppInstaller::Manifest::ManifestTypeEnum::Version: + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_1_VERSION), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + break; + case AppInstaller::Manifest::ManifestTypeEnum::Installer: + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_1_INSTALLER), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + break; + case AppInstaller::Manifest::ManifestTypeEnum::DefaultLocale: + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_1_DEFAULTLOCALE), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + break; + case AppInstaller::Manifest::ManifestTypeEnum::Locale: + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_1_LOCALE), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + break; + default: + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } + } + else if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) { switch (manifestType) { diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -26,10 +26,7 @@ namespace AppInstaller::Manifest resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Version", manifest.Version); } - if (!manifest.DefaultLocalization.Locale.empty() && !Locale::IsWellFormedBcp47Tag(manifest.DefaultLocalization.Locale)) - { - resultErrors.emplace_back(ManifestError::InvalidBcp47Value, "PackageLocale", manifest.DefaultLocalization.Locale); - } + ValidateManifestLocalization(manifest.ManifestVersion, manifest.DefaultLocalization, resultErrors); // Comparison function to check duplicate installer entry. {installerType, arch, language and scope} combination is the key. // Todo: use the comparator from ManifestComparator when that one is fully implemented. @@ -150,12 +147,30 @@ namespace AppInstaller::Manifest // Validate localizations for (auto const& localization : manifest.Localizations) { - if (!localization.Locale.empty() && !Locale::IsWellFormedBcp47Tag(localization.Locale)) - { - resultErrors.emplace_back(ManifestError::InvalidBcp47Value, "PackageLocale", localization.Locale); - } + ValidateManifestLocalization(manifest.ManifestVersion, localization, resultErrors); } return resultErrors; } + + void ValidateManifestLocalization(const ManifestVer& manifestVersion, const ManifestLocalization& localization, std::vector<ValidationError>& resultErrors) + { + if (!localization.Locale.empty() && !Locale::IsWellFormedBcp47Tag(localization.Locale)) + { + resultErrors.emplace_back(ManifestError::InvalidBcp47Value, "PackageLocale", localization.Locale); + } + + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_1 }) + { + const auto& agreements = localization.Get<Localization::Agreements>(); + for (const auto& agreement : agreements) + { + // At least one must be present + if (agreement.Label.empty() && agreement.AgreementText.empty() && agreement.AgreementUrl.empty()) + { + resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Agreements"); + } + } + } + } } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp @@ -342,6 +342,16 @@ namespace AppInstaller::Manifest std::move(v1CommonFields.begin(), v1CommonFields.end(), std::inserter(result, result.end())); } + + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_1 }) + { + std::vector<FieldProcessInfo> fields_v1_1 = + { + { "Agreements", [this](const YAML::Node& value)->ValidationErrors { return ProcessAgreementsNode(value); } }, + }; + + std::move(fields_v1_1.begin(), fields_v1_1.end(), std::inserter(result, result.end())); + } } return result; @@ -390,6 +400,23 @@ namespace AppInstaller::Manifest return result; } + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetAgreementFieldProcessInfo(const ManifestVer& manifestVersion) + { + std::vector<FieldProcessInfo> result = {}; + + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_1 }) + { + result = + { + { "AgreementLabel", [this](const YAML::Node& value)->ValidationErrors { m_p_agreement->Label = Utility::Trim(value.as<std::string>()); return {}; } }, + { "Agreement", [this](const YAML::Node& value)->ValidationErrors { m_p_agreement->AgreementText = Utility::Trim(value.as<std::string>()); return {}; } }, + { "AgreementUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_agreement->AgreementUrl = Utility::Trim(value.as<std::string>()); return {}; } }, + }; + } + + return result; + } + ValidationErrors ManifestYamlPopulator::ValidateAndProcessFields( const YAML::Node& rootNode, const std::vector<FieldProcessInfo>& fieldInfos) @@ -475,6 +502,28 @@ namespace AppInstaller::Manifest return resultErrors; } + std::vector<ValidationError> ManifestYamlPopulator::ProcessAgreementsNode(const YAML::Node& agreementsNode) + { + ValidationErrors resultErrors; + std::vector<Agreement> agreements; + + for (auto const& entry : agreementsNode.Sequence()) + { + Agreement agreement; + m_p_agreement = &agreement; + auto errors = ValidateAndProcessFields(entry, AgreementFieldInfos); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + agreements.emplace_back(std::move(agreement)); + } + + if (!agreements.empty()) + { + m_p_localization->Add<Localization::Agreements>(std::move(agreements)); + } + + return resultErrors; + } + ValidationErrors ManifestYamlPopulator::PopulateManifestInternal(const YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, bool fullValidation) { m_fullValidation = fullValidation; @@ -490,6 +539,7 @@ namespace AppInstaller::Manifest DependenciesFieldInfos = GetDependenciesFieldProcessInfo(manifestVersion); PackageDependenciesFieldInfos = GetPackageDependenciesFieldProcessInfo(manifestVersion); LocalizationFieldInfos = GetLocalizationFieldProcessInfo(manifestVersion); + AgreementFieldInfos = GetAgreementFieldProcessInfo(manifestVersion); // Populate root m_p_manifest = &manifest; diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -77,6 +77,8 @@ #define APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION ((HRESULT)0x8a15003E) #define APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE ((HRESULT)0x8a15003F) #define APPINSTALLER_CLI_ERROR_STREAM_READ_FAILURE ((HRESULT)0x8a150040) +#define APPINSTALLER_CLI_ERROR_LICENSE_NOT_ACCEPTED ((HRESULT)0x8a150041) +#define APPINSTALLER_CLI_ERROR_PROMPT_INPUT_ERROR ((HRESULT)0x8a150042) namespace AppInstaller { diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -21,6 +21,9 @@ namespace AppInstaller::Manifest // V1 manifest version for GA constexpr std::string_view s_ManifestVersionV1 = "1.0.0"sv; + // V1.1 manifest version + constexpr std::string_view s_ManifestVersionV1_1 = "1.1.0"sv; + // The manifest extension for the MS Store constexpr std::string_view s_MSStoreExtension = "msstore"sv; diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestLocalization.h b/src/AppInstallerCommonCore/Public/winget/ManifestLocalization.h @@ -25,9 +25,17 @@ namespace AppInstaller::Manifest ShortDescription, Description, Tags, + Agreements, Max }; + struct Agreement + { + string_t Label; + string_t AgreementText; + string_t AgreementUrl; + }; + namespace details { template <Localization L> @@ -42,6 +50,12 @@ namespace AppInstaller::Manifest using value_t = std::vector<string_t>; }; + template <> + struct LocalizationMapping<Localization::Agreements> + { + using value_t = std::vector<Agreement>; + }; + // Used to deduce the LocalizationVariant type; making a variant that includes std::monostate and all LocalizationMapping types. template <size_t... I> inline auto Deduce(std::index_sequence<I...>) { return std::variant<std::monostate, typename LocalizationMapping<static_cast<Localization>(I)>::value_t...>{}; } diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h @@ -194,4 +194,5 @@ namespace AppInstaller::Manifest }; std::vector<ValidationError> ValidateManifest(const Manifest& manifest); + void ValidateManifestLocalization(const ManifestVer& manifestVersion, const ManifestLocalization& localization, std::vector<ValidationError>& resultErrors); } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h b/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h @@ -32,6 +32,7 @@ namespace AppInstaller::Manifest std::vector<FieldProcessInfo> DependenciesFieldInfos; std::vector<FieldProcessInfo> PackageDependenciesFieldInfos; std::vector<FieldProcessInfo> LocalizationFieldInfos; + std::vector<FieldProcessInfo> AgreementFieldInfos; // These pointers are referenced in the processing functions in manifest field process info table. AppInstaller::Manifest::Manifest* m_p_manifest = nullptr; @@ -40,6 +41,7 @@ namespace AppInstaller::Manifest AppInstaller::Manifest::DependencyList* m_p_dependencyList = nullptr; AppInstaller::Manifest::Dependency* m_p_packageDependency = nullptr; AppInstaller::Manifest::ManifestLocalization* m_p_localization = nullptr; + AppInstaller::Manifest::Agreement* m_p_agreement = nullptr; // Cache of Installers node and Localization node YAML::Node const* m_p_installersNode = nullptr; @@ -51,6 +53,7 @@ namespace AppInstaller::Manifest std::vector<FieldProcessInfo> GetDependenciesFieldProcessInfo(const ManifestVer& manifestVersion); std::vector<FieldProcessInfo> GetPackageDependenciesFieldProcessInfo(const ManifestVer& manifestVersion); std::vector<FieldProcessInfo> GetLocalizationFieldProcessInfo(const ManifestVer& manifestVersion, bool forRootFields = false); + std::vector<FieldProcessInfo> GetAgreementFieldProcessInfo(const ManifestVer& manifestVersion); // This method takes YAML root node and list of manifest field info. // Yaml lib does not support case insensitive search and it allows duplicate keys. If duplicate keys exist, @@ -62,6 +65,7 @@ namespace AppInstaller::Manifest void ProcessDependenciesNode(DependencyType type, const YAML::Node& rootNode); std::vector<ValidationError> ProcessPackageDependenciesNode(const YAML::Node& rootNode); + std::vector<ValidationError> ProcessAgreementsNode(const YAML::Node& agreementsNode); std::vector<ValidationError> PopulateManifestInternal(const YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, bool fullValidation); }; diff --git a/src/IndexCreationTool/WinGetUtilWrapper.cs b/src/IndexCreationTool/WinGetUtilWrapper.cs @@ -181,7 +181,7 @@ namespace IndexCreationTool { if (disposing) { - if (this.indexHandle != null) + if (this.indexHandle != IntPtr.Zero) { WinGetSQLiteIndexClose(this.indexHandle); } diff --git a/src/ManifestSchema/ManifestSchema.h b/src/ManifestSchema/ManifestSchema.h @@ -5,8 +5,15 @@ #define MANIFESTSCHEMA_RESOURCE_TYPE 200 #define IDX_MANIFEST_SCHEMA_PREVIEW 201 + #define IDX_MANIFEST_SCHEMA_V1_SINGLETON 202 #define IDX_MANIFEST_SCHEMA_V1_VERSION 203 #define IDX_MANIFEST_SCHEMA_V1_INSTALLER 204 #define IDX_MANIFEST_SCHEMA_V1_DEFAULTLOCALE 205 #define IDX_MANIFEST_SCHEMA_V1_LOCALE 206 + +#define IDX_MANIFEST_SCHEMA_V1_1_SINGLETON 207 +#define IDX_MANIFEST_SCHEMA_V1_1_VERSION 208 +#define IDX_MANIFEST_SCHEMA_V1_1_INSTALLER 209 +#define IDX_MANIFEST_SCHEMA_V1_1_DEFAULTLOCALE 210 +#define IDX_MANIFEST_SCHEMA_V1_1_LOCALE 211 diff --git a/src/ManifestSchema/ManifestSchema.rc b/src/ManifestSchema/ManifestSchema.rc @@ -64,8 +64,15 @@ END // Manifest schema // IDX_MANIFEST_SCHEMA_PREVIEW MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\preview\\manifest.0.1.0.json" + IDX_MANIFEST_SCHEMA_V1_SINGLETON MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.0.0\\manifest.singleton.1.0.0.json" IDX_MANIFEST_SCHEMA_V1_VERSION MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.0.0\\manifest.version.1.0.0.json" IDX_MANIFEST_SCHEMA_V1_INSTALLER MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.0.0\\manifest.installer.1.0.0.json" IDX_MANIFEST_SCHEMA_V1_DEFAULTLOCALE MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.0.0\\manifest.defaultLocale.1.0.0.json" IDX_MANIFEST_SCHEMA_V1_LOCALE MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.0.0\\manifest.locale.1.0.0.json" + +IDX_MANIFEST_SCHEMA_V1_1_SINGLETON MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.1.0\\manifest.singleton.1.1.0.json" +IDX_MANIFEST_SCHEMA_V1_1_VERSION MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.1.0\\manifest.version.1.1.0.json" +IDX_MANIFEST_SCHEMA_V1_1_INSTALLER MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.1.0\\manifest.installer.1.1.0.json" +IDX_MANIFEST_SCHEMA_V1_1_DEFAULTLOCALE MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.1.0\\manifest.defaultLocale.1.1.0.json" +IDX_MANIFEST_SCHEMA_V1_1_LOCALE MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.1.0\\manifest.locale.1.1.0.json" diff --git a/src/ManifestSchema/ManifestSchema.vcxitems b/src/ManifestSchema/ManifestSchema.vcxitems @@ -27,5 +27,10 @@ <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.locale.1.0.0.json" /> <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.singleton.1.0.0.json" /> <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.version.1.0.0.json" /> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.1.0\manifest.defaultLocale.1.1.0.json" /> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.1.0\manifest.installer.1.1.0.json" /> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.1.0\manifest.locale.1.1.0.json" /> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.1.0\manifest.singleton.1.1.0.json" /> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.1.0\manifest.version.1.1.0.json" /> </ItemGroup> </Project> \ No newline at end of file diff --git a/src/ManifestSchema/ManifestSchema.vcxitems.filters b/src/ManifestSchema/ManifestSchema.vcxitems.filters @@ -4,6 +4,15 @@ <Filter Include="schema"> <UniqueIdentifier>{d6998b42-8ce1-440a-ad12-8b505a284d7b}</UniqueIdentifier> </Filter> + <Filter Include="schema\preview"> + <UniqueIdentifier>{fe494b58-5e5c-415a-922f-c6e10725e06a}</UniqueIdentifier> + </Filter> + <Filter Include="schema\v1.0.0"> + <UniqueIdentifier>{74616d1e-e987-4c28-bd85-7bbd205ee813}</UniqueIdentifier> + </Filter> + <Filter Include="schema\v1.1.0"> + <UniqueIdentifier>{36190723-3948-462f-97c2-47fa1b770826}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="$(MSBuildThisFileDirectory)ManifestSchema.h" /> @@ -14,22 +23,37 @@ </ItemGroup> <ItemGroup> <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\preview\manifest.0.1.0.json"> - <Filter>schema</Filter> - </None> - <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.defaultLocale.1.0.0.json"> - <Filter>schema</Filter> + <Filter>schema\preview</Filter> </None> <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.installer.1.0.0.json"> - <Filter>schema</Filter> + <Filter>schema\v1.0.0</Filter> + </None> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.defaultLocale.1.0.0.json"> + <Filter>schema\v1.0.0</Filter> </None> <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.locale.1.0.0.json"> - <Filter>schema</Filter> + <Filter>schema\v1.0.0</Filter> </None> <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.singleton.1.0.0.json"> - <Filter>schema</Filter> + <Filter>schema\v1.0.0</Filter> </None> <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.version.1.0.0.json"> - <Filter>schema</Filter> + <Filter>schema\v1.0.0</Filter> + </None> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.1.0\manifest.defaultLocale.1.1.0.json"> + <Filter>schema\v1.1.0</Filter> + </None> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.1.0\manifest.installer.1.1.0.json"> + <Filter>schema\v1.1.0</Filter> + </None> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.1.0\manifest.locale.1.1.0.json"> + <Filter>schema\v1.1.0</Filter> + </None> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.1.0\manifest.singleton.1.1.0.json"> + <Filter>schema\v1.1.0</Filter> + </None> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.1.0\manifest.version.1.1.0.json"> + <Filter>schema\v1.1.0</Filter> </None> </ItemGroup> </Project> \ No newline at end of file