cmdpal_packaging/
lib.rs

1//! This module provides functions to make your extension installable and usable by the Command Palette.
2//! You should invoke these functions in `build.rs` to generate the necessary files for your extension.
3//! The generated files will be placed in the cargo artifacts directory, alongside the final binary.
4
5const WINMD_NAME: &str = "Microsoft.CommandPalette.Extensions.winmd";
6const WINMD_DATA: &[u8] = include_bytes!("Microsoft.CommandPalette.Extensions.winmd");
7
8/// An workaround function to get the cargo final artifact directory.
9///
10/// Taken and modified from https://github.com/rust-lang/cargo/issues/9661#issuecomment-1812847609.
11///
12/// Should get replaced when https://github.com/rust-lang/cargo/issues/13663 lands.
13fn get_cargo_artifact_dir() -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
14    let skip_triple = std::env::var("TARGET")? == std::env::var("HOST")?;
15    let skip_parent_dirs = if skip_triple { 3 } else { 4 };
16
17    let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?);
18    let mut current = out_dir.as_path();
19    for _ in 0..skip_parent_dirs {
20        current = current.parent().ok_or("not found")?;
21    }
22
23    Ok(std::path::PathBuf::from(current))
24}
25
26/// Generates the `Microsoft.CommandPalette.Extensions.winmd` file alongside the final binary.
27/// This file is necessary for interoperability with the Command Palette.
28pub fn generate_winmd() -> Result<(), Box<dyn std::error::Error>> {
29    let artifact_dir = get_cargo_artifact_dir()?;
30    let winmd_path = std::path::Path::new(&artifact_dir).join(WINMD_NAME);
31    std::fs::write(&winmd_path, WINMD_DATA)?;
32    Ok(())
33}
34
35/// Description struct for the AppxManifest XML file.
36/// This file is necessary to install the extension as a packaged app on Windows,
37/// allowing it to be recognized by the Command Palette.
38pub struct AppxManifest {
39    id: String,
40    publisher_id: String,
41    version: String,
42    logo: String,
43    display_name: String,
44    publisher_display_name: String,
45    description: String,
46    executable: String,
47    arguments: String,
48    classes: Vec<(String, String)>, // (ClassId, DisplayName)
49}
50
51impl AppxManifest {
52    /// Generates the AppxManifest XML string.
53    pub fn generate_xml(&self) -> String {
54        let com_classes: Vec<String> = self
55            .classes
56            .iter()
57            .map(|(class_id, display)| {
58                format!(
59                    r#"<com:Class Id="{}" DisplayName="{}" />"#,
60                    class_id, display
61                )
62            })
63            .collect();
64        let activation_classes: Vec<String> = self
65            .classes
66            .iter()
67            .map(|(class_id, _)| format!(r#"<CreateInstance ClassId="{}" />"#, class_id))
68            .collect();
69
70        format!(
71            r#"<?xml version="1.0" encoding="utf-8"?>
72
73<Package
74  xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
75  xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
76  xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
77  xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
78  xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
79  IgnorableNamespaces="uap uap3 rescap">
80
81  <Identity
82    Name="{id}"
83    Publisher="{publisher_id}"
84    Version="{version}" />
85
86  <Properties>
87    <DisplayName>{display_name}</DisplayName>
88    <PublisherDisplayName>{publisher_display_name}</PublisherDisplayName>
89    <Logo>{logo}</Logo>
90  </Properties>
91
92  <Dependencies>
93    <TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
94    <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
95  </Dependencies>
96
97  <Applications>
98    <Application Id="App"
99      Executable="{executable}"
100      EntryPoint="Windows.FullTrustApplication">
101      <uap:VisualElements
102        DisplayName="{display_name}"
103        Description="{description}"
104        BackgroundColor="transparent"
105        Square150x150Logo="Assets\Square150x150Logo.png"
106        Square44x44Logo="Assets\Square44x44Logo.png">
107      </uap:VisualElements>
108      <Extensions>
109        <com:Extension Category="windows.comServer">
110          <com:ComServer>
111            <com:ExeServer Executable="{executable}" Arguments="{arguments}" DisplayName="{display_name}">
112              {com_classes}
113            </com:ExeServer>
114          </com:ComServer>
115        </com:Extension>
116        <uap3:Extension Category="windows.appExtension">
117          <uap3:AppExtension Name="com.microsoft.commandpalette"
118            Id="PG-SP-ID"
119            PublicFolder="Public"
120            DisplayName="{display_name}"
121            Description="{description}">
122            <uap3:Properties>
123              <CmdPalProvider>
124                <Activation>
125                  {activation_classes}
126                </Activation>
127                <SupportedInterfaces>
128                  <Commands/>
129                </SupportedInterfaces>
130              </CmdPalProvider>
131            </uap3:Properties>
132          </uap3:AppExtension>
133        </uap3:Extension>
134      </Extensions>
135    </Application>
136  </Applications>
137
138  <Capabilities>
139    <rescap:Capability Name="runFullTrust" />
140  </Capabilities>
141</Package>
142"#,
143            id = self.id,
144            publisher_id = self.publisher_id,
145            display_name = self.display_name,
146            publisher_display_name = self.publisher_display_name,
147            version = self.version,
148            description = self.description,
149            logo = self.logo,
150            executable = self.executable,
151            arguments = self.arguments,
152            com_classes = com_classes.join("\n"),
153            activation_classes = activation_classes.join("\n"),
154        )
155    }
156
157    /// Writes the AppxManifest XML to the cargo artifact directory.
158    pub fn write_xml(&self) -> Result<(), Box<dyn std::error::Error>> {
159        let artifact_dir = get_cargo_artifact_dir()?;
160        let manifest_path = artifact_dir.join("AppxManifest.xml");
161        std::fs::write(&manifest_path, self.generate_xml())?;
162        Ok(())
163    }
164}
165
166/// Builder for creating an AppxManifest.
167#[derive(Default)]
168pub struct AppxManifestBuilder {
169    id: Option<String>,
170    publisher_id: Option<String>,
171    version: Option<String>,
172    logo: Option<String>,
173    display_name: Option<String>,
174    publisher_display_name: Option<String>,
175    description: Option<String>,
176    executable: Option<String>,
177    arguments: Option<String>,
178    classes: Vec<(String, Option<String>)>, // (ClassId, DisplayName)
179}
180
181impl AppxManifestBuilder {
182    /// Creates a new AppxManifestBuilder with default values.
183    pub fn new() -> Self {
184        Self::default()
185    }
186
187    /// Sets the identity name.
188    /// This is the unique identifier for the app.
189    /// See: https://learn.microsoft.com/en-us/uwp/schemas/appxpackage/uapmanifestschema/element-identity#attributes
190    pub fn id(mut self, id: impl Into<String>) -> Self {
191        self.id = Some(id.into());
192        self
193    }
194
195    /// Sets the publisher identity.
196    /// Defaults to "CN=Unknown" if not set.
197    /// See: https://learn.microsoft.com/en-us/uwp/schemas/appxpackage/uapmanifestschema/element-identity#attributes
198    pub fn publisher_id(mut self, publisher_id: impl Into<String>) -> Self {
199        self.publisher_id = Some(publisher_id.into());
200        self
201    }
202
203    /// Sets the version of the app.
204    /// The version should be in the format "x.y.z.p".
205    /// X mustn't be zero when publishing.
206    /// See: https://learn.microsoft.com/en-us/uwp/schemas/appxpackage/uapmanifestschema/element-identity#attributes
207    /// Defaults to the value of `CARGO_PKG_VERSION`, stripped of any suffixes, and with a ".0" suffix added.
208    /// For example, if `CARGO_PKG_VERSION` is "1.2.3-alpha", the version will be "1.2.3.0".
209    pub fn version(mut self, version: impl Into<String>) -> Self {
210        self.version = Some(version.into());
211        self
212    }
213
214    /// Sets the logo path.
215    /// Defaults to Assets\StoreLogo.png if not set.
216    pub fn logo(mut self, logo: impl Into<String>) -> Self {
217        self.logo = Some(logo.into());
218        self
219    }
220
221    /// Sets the display name of the app.
222    /// Defaults to the identity name if not set.
223    pub fn display_name(mut self, display_name: impl Into<String>) -> Self {
224        self.display_name = Some(display_name.into());
225        self
226    }
227
228    /// Sets the publisher display name.
229    /// Defaults to "Unknown" if not set.
230    pub fn publisher_display_name(mut self, publisher_display_name: impl Into<String>) -> Self {
231        self.publisher_display_name = Some(publisher_display_name.into());
232        self
233    }
234
235    /// Sets the description of the app.
236    pub fn description(mut self, description: impl Into<String>) -> Self {
237        self.description = Some(description.into());
238        self
239    }
240
241    /// Sets the executable path.
242    /// `infer_executable` will be used if not set which defaults to `CARGO_BIN_NAME.exe`, however it's very unusable.
243    /// Normally should be `$YOUR_CRATE_NAME.exe`.
244    pub fn executable(mut self, executable: impl Into<String>) -> Self {
245        self.executable = Some(executable.into());
246        self
247    }
248
249    /// Sets the arguments for the executable when executing.
250    /// Defaults to `-RegisterAsComServer` if not set.
251    /// This argument is used to register the COM server for the extension.
252    pub fn arguments(mut self, arguments: impl Into<String>) -> Self {
253        self.arguments = Some(arguments.into());
254        self
255    }
256
257    /// Adds a extension class with a string class GUID.
258    /// The display name is optional and will default to the display name of the app if not provided.
259    /// It's recommended to provide a display name when registering multiple extension classes.
260    pub fn class(mut self, class_id: impl Into<String>, display_name: Option<&str>) -> Self {
261        let display_name = display_name.map(|d| d.into());
262        self.classes.push((class_id.into(), display_name));
263        self
264    }
265
266    /// Adds a extension class with a u128 class GUID.
267    /// The display name is optional and will default to the display name of the app if not provided.
268    /// It's recommended to provide a display name when registering multiple extension classes.
269    pub fn class_u128(self, class_id: u128, display_name: Option<&str>) -> Self {
270        let class_id = format!(
271            "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
272            (class_id >> 96) as u32,
273            (class_id >> 80) as u16,
274            (class_id >> 64) as u16,
275            (class_id >> 48) as u16,
276            class_id & 0xFFFFFFFFFFFF
277        );
278        self.class(class_id, display_name)
279    }
280
281    fn infer_executable() -> String {
282        std::env::var("CARGO_BIN_NAME").unwrap_or_else(|_| {
283            println!("cargo::warning=CARGO_BIN_NAME is not set, using 'cmdpal-extension'");
284            "cmdpal-extension".into()
285        }) + ".exe"
286    }
287
288    fn infer_version() -> String {
289        let version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| {
290            println!("cargo::warning=CARGO_PKG_VERSION is not set, using '0.1.0' as base");
291            "0.1.0".into()
292        });
293
294        version
295            .split_once('-')
296            .map_or_else(|| version.as_str(), |(v, _)| v)
297            .split('.')
298            .map(|s| s.to_string()) // handle cases like "0.1.0-alpha" or "1.2.3-beta"
299            .collect::<Vec<String>>()
300            .join(".")
301            + ".0" // Convert x.x.x  to x.x.x.0
302    }
303
304    /// Builds the AppxManifest with the provided values.
305    pub fn build(self) -> AppxManifest {
306        let id = self.id.expect("id is required");
307        let publisher_id = self.publisher_id.unwrap_or_else(|| {
308            println!("cargo::warning=publisher_id is not set, using default 'CN=Unknown'");
309            "CN=Unknown".into()
310        });
311        let version = self.version.unwrap_or_else(Self::infer_version);
312        let logo = self.logo.unwrap_or("Assets\\StoreLogo.png".into());
313        let display_name = self.display_name.unwrap_or_else(|| id.clone());
314        let publisher_display_name = self.publisher_display_name.unwrap_or("Unknown".into());
315        let description = self.description.unwrap_or_else(|| display_name.clone());
316        let executable = self.executable.unwrap_or_else(Self::infer_executable);
317        let arguments = self.arguments.unwrap_or("-RegisterAsComServer".into());
318        let classes: Vec<(String, String)> = self
319            .classes
320            .into_iter()
321            .map(|(class_id, display)| {
322                let display = display.unwrap_or_else(|| display_name.clone());
323                (class_id, display)
324            })
325            .collect();
326
327        AppxManifest {
328            id,
329            publisher_id,
330            version,
331            logo,
332            display_name,
333            publisher_display_name,
334            description,
335            executable,
336            arguments,
337            classes,
338        }
339    }
340}