Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tugger-wix: Add option to add start menu shortcut. #714

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyoxidizer/src/py_packaging/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ pub trait PythonBinaryBuilder {
/// The name of the binary.
fn name(&self) -> String;

/// Set the name of the binary.
fn set_name(&mut self, value: &str) -> Result<()>;

/// How the binary will link against libpython.
fn libpython_link_mode(&self) -> LibpythonLinkMode;

Expand Down
6 changes: 6 additions & 0 deletions pyoxidizer/src/py_packaging/standalone_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,12 @@ impl PythonBinaryBuilder for StandalonePythonExecutableBuilder {
self.exe_name.clone()
}

fn set_name(&mut self, value: &str) -> Result<()> {
self.exe_name = value.to_string();

Ok(())
}

fn libpython_link_mode(&self) -> LibpythonLinkMode {
self.link_mode
}
Expand Down
30 changes: 30 additions & 0 deletions pyoxidizer/src/starlark/python_executable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ impl TypedValue for PythonExecutableValue {
Ok(Value::from(exe.windows_runtime_dlls_mode().to_string()))
}
"windows_subsystem" => Ok(Value::from(exe.windows_subsystem())),
"name" => Ok(Value::from(exe.name())),
_ => Err(ValueError::OperationNotSupported {
op: UnsupportedOperation::GetAttr(attribute.to_string()),
left: Self::TYPE.to_string(),
Expand All @@ -204,6 +205,7 @@ impl TypedValue for PythonExecutableValue {
| "tcl_files_path"
| "windows_runtime_dlls_mode"
| "windows_subsystem"
| "name"
))
}

Expand Down Expand Up @@ -260,6 +262,11 @@ impl TypedValue for PythonExecutableValue {

Ok(())
}
"name" => {
let _ = exe.set_name(value.to_string().as_str());

Ok(())
}
_ => Err(ValueError::OperationNotSupported {
op: UnsupportedOperation::SetAttr(attribute.to_string()),
left: Self::TYPE.to_string(),
Expand Down Expand Up @@ -962,6 +969,8 @@ impl PythonExecutableValue {

builder.add_program_files_manifest(type_values, call_stack, manifest.deref().clone())?;

let _ = builder.set_exe_name(self.get_attr("name").unwrap().to_str())?;

Ok(builder_value.clone())
}

Expand Down Expand Up @@ -1385,6 +1394,27 @@ mod tests {
Ok(())
}

#[test]
fn name() -> Result<()> {
let mut env = test_evaluation_context_builder()?.into_context()?;
add_exe(&mut env)?;

let v = env.eval("exe.name")?;
assert_eq!(v.get_type(), "string");
assert_eq!(v.to_string(), "COPYING.txt");

env.eval("exe.name = 'myapp'")?;
let v = env.eval("exe.name")?;
assert_eq!(v.get_type(), "string");
assert_eq!(v.to_string(), "myapp");

env.eval("exe.name = None")?;
let v = env.eval("exe.name")?;
assert_eq!(v.get_type(), "NoneType");

Ok(())
}

#[test]
fn test_windows_runtime_dlls_mode() -> Result<()> {
let mut env = test_evaluation_context_builder()?.into_context()?;
Expand Down
109 changes: 109 additions & 0 deletions tugger-wix/src/simple_msi_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ pub struct WiXSimpleMsiBuilder {
/// Files to materialize in `Program Files`.
program_files_manifest: FileManifest,

add_to_start_menu: Option<bool>,
exe_name: Option<String>,
upgrade_code: Option<String>,
package_keywords: Option<String>,
package_description: Option<String>,
Expand Down Expand Up @@ -91,6 +93,15 @@ impl WiXSimpleMsiBuilder {
Ok(())
}

/// Set the `exe_name` attribute value.
///
/// This should be set to the name of the exe file to add to the start menu shortcut.
pub fn set_exe_name(&mut self, value: String) -> Result<()> {
self.exe_name = Some(value);

Ok(())
}

/// Attempt to add the Visual C++ Redistributable DLLs to the program files manifest.
///
/// This will use `vswhere.exe` to attempt to locate a Visual Studio installation
Expand All @@ -117,6 +128,13 @@ impl WiXSimpleMsiBuilder {
Ok(())
}

/// Set the `<Add to Start Menu` attribute value.
#[must_use]
pub fn add_to_start_menu(mut self, value: bool) -> Self {
self.add_to_start_menu = Some(value);
self
}

/// Set the `<Product UpgradeCode` attribute value.
///
/// If not called, a deterministic value will be derived from the product name.
Expand Down Expand Up @@ -410,8 +428,72 @@ impl WiXSimpleMsiBuilder {

writer.write(XmlEvent::end_element().name("Directory"))?;
writer.write(XmlEvent::end_element().name("Directory"))?;
if self.add_to_start_menu.unwrap_or(false) {

writer.write(XmlEvent::start_element("Directory").attr("Id", "ProgramMenuFolder"))?;
writer.write(XmlEvent::end_element().name("Directory"))?;
}

writer.write(XmlEvent::end_element().name("Directory"))?;

if self.add_to_start_menu.unwrap_or(false) {
let shortcut_target = &self.exe_name.as_ref().unwrap_or_else(|| panic!("exe_name not set"));
writer.write(XmlEvent::start_element("DirectoryRef").attr("Id", "ProgramMenuFolder"))?;
writer.write(
XmlEvent::start_element("Component")
.attr("Id", "ApplicationShortcut")
.attr("Guid", &self.shortcut_guid())
)?;

let full_target = &format!("[APPLICATIONFOLDER]{}.exe", shortcut_target);

let shortcut = XmlEvent::start_element("Shortcut")
.attr("Id", "ApplicationStartMenuShortcut")
.attr("Name", &self.product_name)
.attr("WorkingDirectory", "APPLICATIONFOLDER")
.attr("Target", full_target)
;

let shortcut = if let Some(description) = &self.package_description {
shortcut.attr("Description", description)
} else {
shortcut
};

let shortcut = if self.product_icon.is_some() {
shortcut.attr("Icon", "ProductICO")
} else {
shortcut
};

writer.write(shortcut)?;
writer.write(XmlEvent::end_element().name("Shortcut"))?;
writer.write(
XmlEvent::start_element("RemoveFolder")
.attr("Id", "ApplicationProgramsFolder")
.attr("On", "uninstall")
)?;
writer.write(XmlEvent::end_element().name("RemoveFolder"))?;
writer.write(
XmlEvent::start_element("RegistryValue")
.attr("Root", "HKCU")
.attr("Key", &format!(
"Software\\{}\\{}",
&self.product_manufacturer,
&self.product_name
))
.attr("Name", "installed")
.attr("Type", "integer")
.attr("Value", "1")
.attr("KeyPath", "yes")
)?;
writer.write(XmlEvent::end_element().name("RegistryValue"))?;


writer.write(XmlEvent::end_element().name("Component"))?;
writer.write(XmlEvent::end_element().name("DirectoryRef"))?;
}

writer.write(
XmlEvent::start_element("Feature")
.attr("Id", "MainProgram")
Expand All @@ -424,6 +506,23 @@ impl WiXSimpleMsiBuilder {
.attr("Absent", "disallow"),
)?;

if self.add_to_start_menu.unwrap_or(false) {
writer.write(
XmlEvent::start_element("Feature")
.attr("Id", "ApplicationShortcutFeature")
.attr("Title", "Add Start Menu Shortcut")
.attr(
"Description",
"Adds the application to the Start Menu",
)
.attr("Level", "1")
.attr("Absent", "allow"),
)?;
writer.write(XmlEvent::start_element("ComponentRef").attr("Id", "ApplicationShortcut"))?;
writer.write(XmlEvent::end_element().name("ComponentRef"))?;
writer.write(XmlEvent::end_element().name("Feature"))?;
}

// Add group for all files derived from self.program_files_manifest.
writer.write(
XmlEvent::start_element("ComponentGroupRef")
Expand Down Expand Up @@ -571,6 +670,16 @@ impl WiXSimpleMsiBuilder {
.encode_upper(&mut Uuid::encode_buffer())
.to_string()
}

fn shortcut_guid(&self) -> String {
Uuid::new_v5(
&Uuid::NAMESPACE_DNS,
format!("tugger.application_shortcut.{}", self.product_name).as_bytes(),
)
.as_hyphenated()
.encode_upper(&mut Uuid::encode_buffer())
.to_string()
}
}

#[cfg(test)]
Expand Down
16 changes: 16 additions & 0 deletions tugger/docs/tugger_starlark_type_wix_msi_builder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,22 @@

If not set, the default is ``<product_name>-<product_version>.msi``.

.. py:attribute:: add_to_start_menu

(``bool``)

If enabled a shortcut wil be created in the Start Menu pointing to ``exe_name``
eg. ``msi.add_to_start_menu = True``

If not set no shortcut will be installed.

.. py:attribute:: exe_name

(``str``)

The filename of the installed application, used by add_to_start_menu.
This is automatically set when using ``exe.to_wix_msi_builder()``.

.. py:attribute:: package_description

(``str``)
Expand Down
16 changes: 16 additions & 0 deletions tugger/src/starlark/wix_msi_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ impl TypedValue for WiXMsiBuilderValue {
"upgrade_code" => {
inner.builder = inner.builder.clone().upgrade_code(value.to_string());
}
"add_to_start_menu" => {
inner.builder = inner.builder.clone().add_to_start_menu(value.to_bool());
}
"set_exe_name" => {
let _ = inner.builder.set_exe_name(value.to_string());
}
attr => {
return Err(ValueError::OperationNotSupported {
op: UnsupportedOperation::SetAttr(attr.to_string()),
Expand Down Expand Up @@ -325,6 +331,16 @@ impl WiXMsiBuilderValue {
})
}

pub fn set_exe_name(&mut self, exe_name: String) -> ValueResult {
const LABEL: &str = "WiXMSIBuilder.set_exe_name()";

let mut inner = self.inner(LABEL)?;

let _ = inner.builder.set_exe_name(exe_name);

Ok(Value::new(NoneType::None))
}

pub fn to_file_content(
&self,
type_values: &TypeValues,
Expand Down