diff --git a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md index 6bd5ee93a..e478926df 100644 --- a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md +++ b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md @@ -115,6 +115,52 @@ changedProperties: - capabilities ``` +## Install a feature on demand using an offline source + +To allow DISM to install capabilities that are not present on the machine in an offline +environment, add the offline source path to the `sourcePaths` property. + +```powershell +$instance = @{ + sourcePaths = @('z:\sources\SxS') + capabilities = @( + @{ + identity = 'NetFX3~~~~' + state = 'Installed' + } + ) +} | ConvertTo-Json -Depth 3 + +dsc resource set --resource Microsoft.Windows/FeatureOnDemandList --input $instance +``` + +When the resource installs the capability, DSC returns the updated state: + +```yaml +beforeState: + sourcePaths: + - z:\sources\SxS + capabilities: + - identity: NetFX3~~~~ + state: NotPresent + displayName: '' + description: '' + downloadSize: 0 + installSize: 487706170 +afterState: + sourcePaths: + - z:\sources\SxS + capabilities: + - identity: NetFX3~~~~ + state: Installed + displayName: '' + description: '' + downloadSize: 0 + installSize: 487706170 +changedProperties: +- capabilities +``` + ## Manage multiple capabilities in a single operation You can install or remove multiple capabilities in a single **Set** call by specifying multiple diff --git a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md index 1e5e0bbde..bcd48709a 100644 --- a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md +++ b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md @@ -96,6 +96,12 @@ The following list describes the properties for the resource. - [capabilities](#capabilities) - An array of capability entries. +- **Instance properties:** The following properties are optional. + They define the desired state for an instance of the resource. + + - [sourcePaths](#sourcePaths) - The location of the source files to use for installation if + necessary. + - **Read-only properties:** The resource returns the following properties, but they aren't configurable. For more information about read-only properties, see the "Read-only resource properties" section in [DSC resource properties][05]. @@ -277,6 +283,26 @@ IsReadOnly : true The size in bytes that the capability occupies on disk after installation. This property is returned by **Get** and **Export** operations. +### sourcePaths + +
Expand for sourcePaths property metadata + +```yaml +Type : array +IsRequired : false +IsKey : false +IsReadOnly : false +``` + +
+ +Supplied at the top level of the **Set** operation, indicates the location of the source files to +use for installation if necessary. The DISM API will search these paths if the feature files are +not available in the local feature store. See the [feature on demand repository documentation][08] +for more information on valid sources. + +This property is optional and will be omitted from the response if no value is provided. + ### _restartRequired
Expand for _restartRequired property metadata @@ -314,6 +340,12 @@ The following snippet contains the JSON Schema that validates an instance of the "additionalProperties": true } }, + "sourcePaths": { + "type": "array", + "items": { + "type": "string" + } + }, "capabilities": { "type": "array", "items": { @@ -378,3 +410,4 @@ Common causes include: [05]: ../../../../../concepts/resources/properties.md#read-only-resource-properties [06]: ../OptionalFeatureList/index.md [07]: /windows-hardware/manufacture/desktop/features-on-demand-v2--capabilities +[08]: /windows-hardware/manufacture/desktop/features-on-demand-v2--capabilities?view=windows-11#fod-repositories \ No newline at end of file diff --git a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md index 465597108..742ade55d 100644 --- a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md +++ b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md @@ -146,6 +146,50 @@ changedProperties: - features ``` +## Enable an optional feature using an offline source + +To allow DISM to install features that are not present on the machine in an offline environment, +add the offline source path to the `sourcePaths` property. + +```powershell +$instance = @{ + sourcePaths = @('z:\sources\SxS') + features = @( + @{ + featureName = 'NetFx3' + state = 'Installed' + } + ) +} | ConvertTo-Json -Depth 3 + +dsc resource set --resource Microsoft.Windows/OptionalFeatureList --input $instance +``` + +When the resource enables the feature, DSC returns the updated state: + +```yaml +beforeState: + sourcePaths: + - z:\sources\SxS + features: + - featureName: NetFx3 + state: NotPresent + displayName: NET Framework 3.5 (includes .NET 2.0 and 3.0) + description: .NET Framework 3.5 (includes .NET 2.0 and 3.0) + restartRequired: No +afterState: + sourcePaths: + - z:\sources\SxS + features: + - featureName: NetFx3 + state: Installed + displayName: NET Framework 3.5 (includes .NET 2.0 and 3.0) + description: .NET Framework 3.5 (includes .NET 2.0 and 3.0) + restartRequired: Possible +changedProperties: +- features +``` + ## Manage multiple features in a single operation You can enable or disable multiple features in a single **Set** call by specifying multiple entries diff --git a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md index f08d4bd47..8336b06d5 100644 --- a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md +++ b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md @@ -94,6 +94,12 @@ The following list describes the properties for the resource. - [features](#features) - An array of optional feature entries. +- **Instance properties:** The following properties are optional. + They define the desired state for an instance of the resource. + + - [sourcePaths](#sourcePaths) - The location of the source files to use for installation if + necessary. + - **Read-only properties:** The resource returns the following properties, but they aren't configurable. For more information about read-only properties, see the "Read-only resource properties" section in [DSC resource properties][05]. @@ -262,6 +268,26 @@ property is returned by **Get** and **Export** operations and cannot be set. | `Possible` | A restart may be required depending on system conditions. | | `Required` | A restart is required to complete the state change. | +### sourcePaths + +
Expand for sourcePaths property metadata + +```yaml +Type : array +IsRequired : false +IsKey : false +IsReadOnly : false +``` + +
+ +Supplied at the top level of the **Set** operation, indicates the location of the source files to +use for installation if necessary. The DISM API will search these paths if the feature files are +not available in the local feature store. See the [DISM enable feature documentation][08] for more +information on valid sources. + +This property is optional and will be omitted from the response if no value is provided. + ### _restartRequired
Expand for _restartRequired property metadata @@ -298,6 +324,12 @@ The following snippet contains the JSON Schema that validates an instance of the "type": "object", "additionalProperties": true } + }, + "sourcePaths": { + "type": "array", + "items": { + "type": "string" + } }, "features": { "type": "array", @@ -364,4 +396,5 @@ Common causes include: [04]: ./examples/export-optional-features.md [05]: ../../../../../concepts/resources/properties.md#read-only-resource-properties [06]: ../FeatureOnDemandList/index.md -[07]: /windows-server/administration/windows-commands/dism/dism-operating-system-package-servicing-command-line-options +[07]: /windows-hardware/manufacture/desktop/deployment-image-servicing-and-management--dism--command-line-options +[08]: /windows-hardware/manufacture/desktop/dism-operating-system-package-servicing-command-line-options#enable-feature \ No newline at end of file diff --git a/resources/dism_dsc/featureondemand.dsc.resource.json b/resources/dism_dsc/featureondemand.dsc.resource.json index 0ab06bf78..9a5ae0bad 100644 --- a/resources/dism_dsc/featureondemand.dsc.resource.json +++ b/resources/dism_dsc/featureondemand.dsc.resource.json @@ -116,6 +116,14 @@ } } } + }, + "sourcePaths": { + "type": "array", + "title": "Source paths", + "description": "An array of paths to feature files if the files are not available in the local feature store. This is an optional property for set operations and is ignored for get operations.", + "items": { + "type": "string" + } } } } diff --git a/resources/dism_dsc/optionalfeature.dsc.resource.json b/resources/dism_dsc/optionalfeature.dsc.resource.json index 77c4b82c9..2bafa88d1 100644 --- a/resources/dism_dsc/optionalfeature.dsc.resource.json +++ b/resources/dism_dsc/optionalfeature.dsc.resource.json @@ -91,7 +91,8 @@ "PartiallyInstalled" ], "title": "Feature state", - "description": "The current state of the optional feature. For set operations, only Installed, NotPresent, and Removed are accepted; other states are returned by get and export operations and are not valid inputs for set."}, + "description": "The current state of the optional feature. For set operations, only Installed, NotPresent, and Removed are accepted; other states are returned by get and export operations and are not valid inputs for set." + }, "displayName": { "type": "string", "title": "Display name", @@ -114,6 +115,14 @@ } } } + }, + "sourcePaths": { + "type": "array", + "title": "Source paths", + "description": "An array of paths to feature files if the files are not available in the local feature store. This is an optional property for set operations and is ignored for get operations.", + "items": { + "type": "string" + } } } } diff --git a/resources/dism_dsc/src/dism.rs b/resources/dism_dsc/src/dism.rs index aa693d8b0..9e81f4df6 100644 --- a/resources/dism_dsc/src/dism.rs +++ b/resources/dism_dsc/src/dism.rs @@ -319,8 +319,21 @@ impl DismSessionHandle { } /// Returns `Ok(true)` if DISM reports a reboot is required (HRESULT 3010). - pub fn enable_feature(&self, feature_name: &str) -> Result { + pub fn enable_feature(&self, feature_name: &str, source_paths: &Option>) -> Result { let wide_name = to_wide_null(feature_name); + + let wide_source_paths: Option>> = source_paths + .as_ref() + .filter(|paths| !paths.is_empty()) + .map(|paths| paths.iter().map(|p| to_wide_null(p)).collect()); + + let sources: Option> = wide_source_paths + .as_ref() + .map(|paths| paths.iter().map(|p| p.as_ptr()).collect()); + + let source_count = sources.as_ref().map_or(0, |paths| paths.len() as u32); + let sources_ptr = sources.as_ref().map_or(std::ptr::null(), |v| v.as_ptr()); + let hr = unsafe { (self.api.enable_feature)( self.handle, @@ -328,8 +341,8 @@ impl DismSessionHandle { std::ptr::null(), // Identifier DISM_PACKAGE_NONE, // PackageIdentifier 0, // LimitAccess = FALSE - std::ptr::null(), // SourcePaths - 0, // SourcePathCount + sources_ptr, // SourcePaths + source_count, // SourcePathCount 0, // EnableAll = FALSE std::ptr::null_mut(), // CancelEvent std::ptr::null_mut(), // Progress @@ -457,18 +470,31 @@ impl DismSessionHandle { } /// Returns `Ok(true)` if DISM reports a reboot is required (HRESULT 3010). - pub fn add_capability(&self, name: &str) -> Result { + pub fn add_capability(&self, name: &str, source_paths: &Option>) -> Result { let add_cap = self.api.add_capability .ok_or_else(|| t!("dism.capabilitiesNotSupported").to_string())?; let wide_name = to_wide_null(name); + + let wide_source_paths: Option>> = source_paths + .as_ref() + .filter(|paths| !paths.is_empty()) + .map(|paths| paths.iter().map(|p| to_wide_null(p)).collect()); + + let sources: Option> = wide_source_paths + .as_ref() + .map(|paths| paths.iter().map(|p| p.as_ptr()).collect()); + + let source_count = sources.as_ref().map_or(0, |paths| paths.len() as u32); + let sources_ptr = sources.as_ref().map_or(std::ptr::null(), |v| v.as_ptr()); + let hr = unsafe { add_cap( self.handle, wide_name.as_ptr(), 0, // LimitAccess = FALSE - std::ptr::null(), // SourcePaths - 0, // SourcePathCount + sources_ptr, // SourcePaths + source_count, // SourcePathCount std::ptr::null_mut(), // CancelEvent std::ptr::null_mut(), // Progress std::ptr::null_mut(), // UserData diff --git a/resources/dism_dsc/src/feature_on_demand/export.rs b/resources/dism_dsc/src/feature_on_demand/export.rs index 6709a6a7e..ed1f8bf2d 100644 --- a/resources/dism_dsc/src/feature_on_demand/export.rs +++ b/resources/dism_dsc/src/feature_on_demand/export.rs @@ -91,7 +91,7 @@ pub fn handle_export(input: &str) -> Result { } } - let output = FeatureOnDemandList { restart_required_meta: None, capabilities: results }; + let output = FeatureOnDemandList { restart_required_meta: None, source_paths: None, capabilities: results }; serde_json::to_string(&output) .map_err(|e| t!("fod_export.failedSerializeOutput", err = e.to_string()).to_string()) } diff --git a/resources/dism_dsc/src/feature_on_demand/get.rs b/resources/dism_dsc/src/feature_on_demand/get.rs index db5087bd3..d6cddb9e1 100644 --- a/resources/dism_dsc/src/feature_on_demand/get.rs +++ b/resources/dism_dsc/src/feature_on_demand/get.rs @@ -44,7 +44,11 @@ pub fn handle_get(input: &str) -> Result { results.push(info); } - let output = FeatureOnDemandList { restart_required_meta: None, capabilities: results }; + let output = FeatureOnDemandList { + restart_required_meta: None, + source_paths: None, + capabilities: results + }; serde_json::to_string(&output) .map_err(|e| t!("fod_get.failedSerializeOutput", err = e.to_string()).to_string()) } diff --git a/resources/dism_dsc/src/feature_on_demand/set.rs b/resources/dism_dsc/src/feature_on_demand/set.rs index afd055677..a74b3839c 100644 --- a/resources/dism_dsc/src/feature_on_demand/set.rs +++ b/resources/dism_dsc/src/feature_on_demand/set.rs @@ -43,7 +43,7 @@ pub fn handle_set(input: &str) -> Result { CapabilityState::Installed => { match current_state { Some(CapabilityState::Installed) => false, - _ => session.add_capability(identity)?, + _ => session.add_capability(identity, &capability_list.source_paths)?, } } CapabilityState::NotPresent => { @@ -84,7 +84,11 @@ pub fn handle_set(input: &str) -> Result { None }; - let output = FeatureOnDemandList { restart_required_meta, capabilities: results }; + let output = FeatureOnDemandList { + restart_required_meta, + source_paths: capability_list.source_paths, + capabilities: results + }; serde_json::to_string(&output) .map_err(|e| t!("fod_set.failedSerializeOutput", err = e.to_string()).to_string()) } diff --git a/resources/dism_dsc/src/feature_on_demand/types.rs b/resources/dism_dsc/src/feature_on_demand/types.rs index 7b91ee57e..f5d15a1e4 100644 --- a/resources/dism_dsc/src/feature_on_demand/types.rs +++ b/resources/dism_dsc/src/feature_on_demand/types.rs @@ -13,6 +13,8 @@ pub type CapabilityState = DismState; pub struct FeatureOnDemandList { #[serde(rename = "_restartRequired", skip_serializing_if = "Option::is_none")] pub restart_required_meta: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub source_paths: Option>, pub capabilities: Vec, } diff --git a/resources/dism_dsc/src/optional_feature/export.rs b/resources/dism_dsc/src/optional_feature/export.rs index 5b19f95e3..ccb40d2da 100644 --- a/resources/dism_dsc/src/optional_feature/export.rs +++ b/resources/dism_dsc/src/optional_feature/export.rs @@ -88,7 +88,7 @@ pub fn handle_export(input: &str) -> Result { } } - let output = OptionalFeatureList { restart_required_meta: None, features: results }; + let output = OptionalFeatureList { restart_required_meta: None, source_paths: None, features: results }; serde_json::to_string(&output) .map_err(|e| t!("export.failedSerializeOutput", err = e.to_string()).to_string()) } diff --git a/resources/dism_dsc/src/optional_feature/get.rs b/resources/dism_dsc/src/optional_feature/get.rs index bb730cc4d..af4e2fe1f 100644 --- a/resources/dism_dsc/src/optional_feature/get.rs +++ b/resources/dism_dsc/src/optional_feature/get.rs @@ -27,7 +27,11 @@ pub fn handle_get(input: &str) -> Result { results.push(info); } - let output = OptionalFeatureList { restart_required_meta: None, features: results }; + let output = OptionalFeatureList { + restart_required_meta: None, + source_paths: None, + features: results + }; serde_json::to_string(&output) .map_err(|e| t!("get.failedSerializeOutput", err = e.to_string()).to_string()) } diff --git a/resources/dism_dsc/src/optional_feature/set.rs b/resources/dism_dsc/src/optional_feature/set.rs index 46634b8f1..5b25d6991 100644 --- a/resources/dism_dsc/src/optional_feature/set.rs +++ b/resources/dism_dsc/src/optional_feature/set.rs @@ -32,15 +32,9 @@ pub fn handle_set(input: &str) -> Result { .ok_or_else(|| t!("set.stateRequired").to_string())?; let needs_reboot = match desired_state { - FeatureState::Installed => { - session.enable_feature(feature_name)? - } - FeatureState::NotPresent => { - session.disable_feature(feature_name, false)? - } - FeatureState::Removed => { - session.disable_feature(feature_name, true)? - } + FeatureState::Installed => session.enable_feature(feature_name, &feature_list.source_paths)?, + FeatureState::NotPresent => session.disable_feature(feature_name, false)?, + FeatureState::Removed => session.disable_feature(feature_name, true)?, _ => { return Err(t!( "set.unsupportedDesiredState", @@ -64,7 +58,11 @@ pub fn handle_set(input: &str) -> Result { None }; - let output = OptionalFeatureList { restart_required_meta, features: results }; + let output = OptionalFeatureList { + restart_required_meta, + features: results, + source_paths: feature_list.source_paths, + }; serde_json::to_string(&output) .map_err(|e| t!("set.failedSerializeOutput", err = e.to_string()).to_string()) } diff --git a/resources/dism_dsc/src/optional_feature/types.rs b/resources/dism_dsc/src/optional_feature/types.rs index 6415d6a48..b53426e68 100644 --- a/resources/dism_dsc/src/optional_feature/types.rs +++ b/resources/dism_dsc/src/optional_feature/types.rs @@ -13,6 +13,8 @@ pub type FeatureState = DismState; pub struct OptionalFeatureList { #[serde(rename = "_restartRequired", skip_serializing_if = "Option::is_none")] pub restart_required_meta: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub source_paths: Option>, pub features: Vec, }