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="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    <Capability Name="internetClient" />
140    <rescap:Capability Name="runFullTrust" />
141  </Capabilities>
142</Package>
143"#,
144            id = self.id,
145            publisher_id = self.publisher_id,
146            display_name = self.display_name,
147            publisher_display_name = self.publisher_display_name,
148            version = self.version,
149            description = self.description,
150            logo = self.logo,
151            executable = self.executable,
152            arguments = self.arguments,
153            com_classes = com_classes.join("\n"),
154            activation_classes = activation_classes.join("\n"),
155        )
156    }
157
158    /// Writes the AppxManifest XML to the cargo artifact directory.
159    pub fn write_xml(&self) -> Result<(), Box<dyn std::error::Error>> {
160        let artifact_dir = get_cargo_artifact_dir()?;
161        let manifest_path = artifact_dir.join("AppxManifest.xml");
162        std::fs::write(&manifest_path, self.generate_xml())?;
163        Ok(())
164    }
165}
166
167/// Builder for creating an AppxManifest.
168#[derive(Default)]
169pub struct AppxManifestBuilder {
170    id: Option<String>,
171    publisher_id: Option<String>,
172    version: Option<String>,
173    logo: Option<String>,
174    display_name: Option<String>,
175    publisher_display_name: Option<String>,
176    description: Option<String>,
177    executable: Option<String>,
178    arguments: Option<String>,
179    classes: Vec<(String, Option<String>)>, // (ClassId, DisplayName)
180}
181
182impl AppxManifestBuilder {
183    /// Creates a new AppxManifestBuilder with default values.
184    pub fn new() -> Self {
185        Self::default()
186    }
187
188    /// Sets the identity name.
189    /// This is the unique identifier for the app.
190    /// See: https://learn.microsoft.com/en-us/uwp/schemas/appxpackage/uapmanifestschema/element-identity#attributes
191    pub fn id(mut self, id: impl Into<String>) -> Self {
192        self.id = Some(id.into());
193        self
194    }
195
196    /// Sets the publisher identity.
197    /// Defaults to "CN=Unknown" if not set.
198    /// See: https://learn.microsoft.com/en-us/uwp/schemas/appxpackage/uapmanifestschema/element-identity#attributes
199    pub fn publisher_id(mut self, publisher_id: impl Into<String>) -> Self {
200        self.publisher_id = Some(publisher_id.into());
201        self
202    }
203
204    /// Sets the version of the app.
205    /// The version should be in the format "x.y.z.p".
206    /// X mustn't be zero when publishing.
207    /// See: https://learn.microsoft.com/en-us/uwp/schemas/appxpackage/uapmanifestschema/element-identity#attributes
208    /// Defaults to the value of `CARGO_PKG_VERSION`, stripped of any suffixes, and with a ".0" suffix added.
209    /// For example, if `CARGO_PKG_VERSION` is "1.2.3-alpha", the version will be "1.2.3.0".
210    pub fn version(mut self, version: impl Into<String>) -> Self {
211        self.version = Some(version.into());
212        self
213    }
214
215    /// Sets the logo path.
216    /// Defaults to Assets\StoreLogo.png if not set.
217    pub fn logo(mut self, logo: impl Into<String>) -> Self {
218        self.logo = Some(logo.into());
219        self
220    }
221
222    /// Sets the display name of the app.
223    /// Defaults to the identity name if not set.
224    pub fn display_name(mut self, display_name: impl Into<String>) -> Self {
225        self.display_name = Some(display_name.into());
226        self
227    }
228
229    /// Sets the publisher display name.
230    /// Defaults to "Unknown" if not set.
231    pub fn publisher_display_name(mut self, publisher_display_name: impl Into<String>) -> Self {
232        self.publisher_display_name = Some(publisher_display_name.into());
233        self
234    }
235
236    /// Sets the description of the app.
237    pub fn description(mut self, description: impl Into<String>) -> Self {
238        self.description = Some(description.into());
239        self
240    }
241
242    /// Sets the executable path.
243    /// `infer_executable` will be used if not set which defaults to `CARGO_BIN_NAME.exe`, however it's very unusable.
244    /// Normally should be `$YOUR_CRATE_NAME.exe`.
245    pub fn executable(mut self, executable: impl Into<String>) -> Self {
246        self.executable = Some(executable.into());
247        self
248    }
249
250    /// Sets the arguments for the executable when executing.
251    /// Defaults to `-RegisterAsComServer` if not set.
252    /// This argument is used to register the COM server for the extension.
253    pub fn arguments(mut self, arguments: impl Into<String>) -> Self {
254        self.arguments = Some(arguments.into());
255        self
256    }
257
258    /// Adds a extension class with a string class GUID.
259    /// The display name is optional and will default to the display name of the app if not provided.
260    /// It's recommended to provide a display name when registering multiple extension classes.
261    pub fn class(mut self, class_id: impl Into<String>, display_name: Option<&str>) -> Self {
262        let display_name = display_name.map(|d| d.into());
263        self.classes.push((class_id.into(), display_name));
264        self
265    }
266
267    /// Adds a extension class with a u128 class GUID.
268    /// The display name is optional and will default to the display name of the app if not provided.
269    /// It's recommended to provide a display name when registering multiple extension classes.
270    pub fn class_u128(self, class_id: u128, display_name: Option<&str>) -> Self {
271        let class_id = format!(
272            "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
273            (class_id >> 96) as u32,
274            (class_id >> 80) as u16,
275            (class_id >> 64) as u16,
276            (class_id >> 48) as u16,
277            class_id & 0xFFFFFFFFFFFF
278        );
279        self.class(class_id, display_name)
280    }
281
282    fn infer_executable() -> String {
283        let inferred_name = std::env::var("CARGO_PKG_NAME")
284            .ok()
285            .or_else(|| std::env::var("CARGO_BIN_NAME").ok())
286            .unwrap_or("cmdpal-extension".into());
287        println!("cargo::warning=executable is not set, inferred '{}' as default", inferred_name);
288        format!("{}.exe", inferred_name)
289    }
290
291    fn infer_version() -> String {
292        let version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| {
293            println!("cargo::warning=CARGO_PKG_VERSION is not set, using '0.1.0' as base");
294            "0.1.0".into()
295        });
296
297        version
298            .split_once('-')
299            .map_or_else(|| version.as_str(), |(v, _)| v)
300            .split('.')
301            .map(|s| s.to_string()) // handle cases like "0.1.0-alpha" or "1.2.3-beta"
302            .collect::<Vec<String>>()
303            .join(".")
304            + ".0" // Convert x.x.x  to x.x.x.0
305    }
306
307    /// Builds the AppxManifest with the provided values.
308    pub fn build(self) -> AppxManifest {
309        let id = self.id.expect("id is required");
310        let publisher_id = self.publisher_id.unwrap_or_else(|| {
311            println!("cargo::warning=publisher_id is not set, using default 'CN=Unknown'");
312            "CN=Unknown".into()
313        });
314        let version = self.version.unwrap_or_else(Self::infer_version);
315        let logo = self.logo.unwrap_or("Assets\\StoreLogo.png".into());
316        let display_name = self.display_name.unwrap_or_else(|| id.clone());
317        let publisher_display_name = self.publisher_display_name.unwrap_or("Unknown".into());
318        let description = self.description.unwrap_or_else(|| display_name.clone());
319        let executable = self.executable.unwrap_or_else(Self::infer_executable);
320        let arguments = self.arguments.unwrap_or("-RegisterAsComServer".into());
321        let classes: Vec<(String, String)> = self
322            .classes
323            .into_iter()
324            .map(|(class_id, display)| {
325                let display = display.unwrap_or_else(|| display_name.clone());
326                (class_id, display)
327            })
328            .collect();
329
330        AppxManifest {
331            id,
332            publisher_id,
333            version,
334            logo,
335            display_name,
336            publisher_display_name,
337            description,
338            executable,
339            arguments,
340            classes,
341        }
342    }
343}