Skip to content

Feature/binary patch bundle types #13209

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

Open
wants to merge 12 commits into
base: dev
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changes/patch-binaries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
tauri-cli: patch:enhance
---

Binaries are patched before bundling to add the type of a bundle they will placed in. This information will be used during update process to select the correct target.
109 changes: 55 additions & 54 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,54 +1,55 @@
# dependency directories
node_modules/

# Optional npm and yarn cache directory
.npm/
.yarn/

# Output of 'npm pack'
*.tgz

# dotenv environment variables file
.env

# .vscode workspace settings file
.vscode/settings.json
.vscode/launch.json
.vscode/tasks.json

# npm, yarn and bun lock files
package-lock.json
yarn.lock
bun.lockb

# rust compiled folders
target/

# test video for streaming example
streaming_example_test_video.mp4

# examples /gen directory
/examples/**/src-tauri/gen/
/bench/**/src-tauri/gen/

# logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# runtime data
pids
*.pid
*.seed
*.pid.lock

# miscellaneous
/.vs
.DS_Store
.Thumbs.db
*.sublime*
.idea
debug.log
TODO.md
# dependency directories
node_modules/

# Optional npm and yarn cache directory
.npm/
.yarn/

# Output of 'npm pack'
*.tgz

# dotenv environment variables file
.env

# .vscode workspace settings file
.vscode/settings.json
.vscode/launch.json
.vscode/tasks.json

# npm, yarn and bun lock files
package-lock.json
yarn.lock
bun.lockb

# rust compiled folders
target/

# test video for streaming example
streaming_example_test_video.mp4

# examples /gen directory
/examples/**/src-tauri/gen/
/bench/**/src-tauri/gen/

# logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# runtime data
pids
*.pid
*.seed
*.pid.lock

# miscellaneous
/.vs
.DS_Store
.Thumbs.db
*.sublime*
.idea
debug.log
TODO.md
.aider*
14 changes: 13 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/tauri-bundler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dunce = "1"
url = "2"
uuid = { version = "1", features = ["v4", "v5"] }
regex = "1"
goblin = "0.9"

[target."cfg(target_os = \"windows\")".dependencies]
bitness = "0.4"
Expand Down
33 changes: 33 additions & 0 deletions crates/tauri-bundler/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@ mod settings;
mod updater_bundle;
mod windows;

impl From<goblin::error::Error> for crate::error::Error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be done inside crate::error::Error::BinaryParseError by leverating thiserror #[from] attribute

fn from(value: goblin::error::Error) -> Self {
crate::Error::BinaryParseError(value.to_string())
}
}

/// Patch a binary with bundle type information
fn patch_binary(binary: &PathBuf, package_type: &PackageType) -> crate::Result<()> {
log::info!(
"Patching binary {:?} for type {}",
binary,
package_type.short_name()
);
#[cfg(target_os = "linux")]
linux::patch_binary(binary, package_type)?;

#[cfg(target_os = "windows")]
windows::patch_binary(binary, package_type)?;

Ok(())
}

use tauri_utils::display_path;

pub use self::{
Expand Down Expand Up @@ -92,11 +114,21 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
}
}

let main_binary = settings
.binaries()
.iter()
.find(|b| b.main())
.expect("Main binary missing in settings");

let mut bundles = Vec::<Bundle>::new();
for package_type in &package_types {
// bundle was already built! e.g. DMG already built .app
if bundles.iter().any(|b| b.package_type == *package_type) {
continue;
} // REMOVE THIS?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this. This only applies to DMG, right? We don't want to skip anything on linux or windows as the binary needs to be patched for each type anyway.


if settings.updater().is_some() {
patch_binary(&settings.binary_path(main_binary), package_type)?;
}

let bundle_paths = match package_type {
Expand All @@ -119,6 +151,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {

#[cfg(target_os = "windows")]
PackageType::WindowsMsi => windows::msi::bundle_project(settings, false)?,
#[cfg(target_os = "windows")]
PackageType::Nsis => windows::nsis::bundle_project(settings, false)?,

#[cfg(target_os = "linux")]
Expand Down
5 changes: 5 additions & 0 deletions crates/tauri-bundler/src/bundle/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ pub mod appimage;
pub mod debian;
pub mod freedesktop;
pub mod rpm;

mod util;

#[cfg(target_os = "linux")]
pub use util::patch_binary;
62 changes: 62 additions & 0 deletions crates/tauri-bundler/src/bundle/linux/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

/// Change value of __TAURI_BUNDLE_TYPE static variable to mark which package type it was bundled in
#[cfg(target_os = "linux")]
pub fn patch_binary(
binary_path: &std::path::PathBuf,
package_type: &crate::PackageType,
) -> crate::Result<()> {
let mut file_data = std::fs::read(binary_path).expect("Could not binary read file.");

let elf = match goblin::Object::parse(&file_data)? {
goblin::Object::Elf(elf) => elf,
_ => return Err(crate::Error::BinaryParseError("Not an ELF file".into())),
};

if let Some(offset) = find_bundle_type_symbol(elf) {
let offset = offset as usize;
if offset + 3 <= file_data.len() {
let chars = &mut file_data[offset..offset + 3];
match package_type {
crate::PackageType::Deb => chars.copy_from_slice(b"DEB"),
crate::PackageType::Rpm => chars.copy_from_slice(b"RPM"),
crate::PackageType::AppImage => chars.copy_from_slice(b"APP"),
_ => {
return Err(crate::Error::InvalidPackageType(
package_type.short_name().to_owned(),
"linux".to_owned(),
))
}
}
if let Err(error) = std::fs::write(binary_path, &file_data) {
return Err(crate::Error::BinaryWriteError(error.to_string()));
}
} else {
return Err(crate::Error::BinaryOffsetOutOfRange);
}
} else {
return Err(crate::Error::MissingBundleTypeVar);
}

Ok(())
}

/// Find address of a symbol in relocations table
#[cfg(target_os = "linux")]
fn find_bundle_type_symbol(elf: goblin::elf::Elf<'_>) -> Option<i64> {
for sym in elf.syms.iter() {
if let Some(name) = elf.strtab.get_at(sym.st_name) {
if name == "__TAURI_BUNDLE_TYPE" {
for reloc in elf.dynrelas.iter() {
if reloc.r_offset == sym.st_value {
return Some(reloc.r_addend.unwrap());
}
}
}
}
}

None
}
4 changes: 4 additions & 0 deletions crates/tauri-bundler/src/bundle/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#[cfg(target_os = "windows")]
pub mod msi;

pub mod nsis;
pub mod sign;

Expand All @@ -13,3 +14,6 @@ pub use util::{
NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, WIX_OUTPUT_FOLDER_NAME,
WIX_UPDATER_OUTPUT_FOLDER_NAME,
};

#[cfg(target_os = "windows")]
pub use util::patch_binary;
61 changes: 60 additions & 1 deletion crates/tauri-bundler/src/bundle/windows/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use std::{
fs::create_dir_all,
path::{Path, PathBuf},
};

use ureq::ResponseExt;

use crate::utils::http_utils::download;
Expand Down Expand Up @@ -84,3 +83,63 @@ pub fn os_bitness<'a>() -> Option<&'a str> {
_ => None,
}
}

#[cfg(target_os = "windows")]
pub fn patch_binary(binary_path: &PathBuf, package_type: &crate::PackageType) -> crate::Result<()> {
let mut file_data = std::fs::read(binary_path)?;

let pe = match goblin::Object::parse(&file_data)? {
goblin::Object::PE(pe) => pe,
_ => return Err(crate::Error::BinaryParseError("Not an PE file".into())),
};

if let Some(data_ta_section) = pe
.sections
.iter()
.find(|s| s.name().unwrap_or_default() == ".data.ta")
{
let data_offset = data_ta_section.pointer_to_raw_data as usize;

let ptr_bytes = &file_data[data_offset..data_offset + 8];
let ptr_value = u64::from_le_bytes(ptr_bytes.try_into().unwrap());

if let Some(rdata_section) = pe
.sections
.iter()
.find(|s| s.name().unwrap_or_default() == ".rdata")
{
let rva = (ptr_value) - pe.image_base as u64;

let file_offset = rdata_section.pointer_to_raw_data as usize
+ (rva as usize - rdata_section.virtual_address as usize);

if file_offset + 3 <= file_data.len() {
let string_bytes = &mut file_data[file_offset..file_offset + 3];

match package_type {
crate::PackageType::Nsis => string_bytes.copy_from_slice(b"NSS"),
crate::PackageType::WindowsMsi => string_bytes.copy_from_slice(b"MSI"),
_ => {
return Err(crate::Error::InvalidPackageType(
package_type.short_name().to_owned(),
"windows".to_owned(),
))
}
}
if let Err(error) = std::fs::write(binary_path, &file_data) {
return Err(crate::Error::BinaryWriteError(error.to_string()));
}
} else {
return Err(crate::Error::BinaryOffsetOutOfRange);
}
} else {
return Err(crate::Error::BinaryParseError(
"`.rdata' section not found".into(),
));
}
} else {
return Err(crate::Error::MissingBundleTypeVar);
}

Ok(())
}
15 changes: 15 additions & 0 deletions crates/tauri-bundler/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ pub enum Error {
/// Failed to validate downloaded file hash.
#[error("hash mismatch of downloaded file")]
HashError,
/// Failed to parse binary
#[error("Binary parse error: `{0}`")]
BinaryParseError(String),
/// Package type is not supported by target platform
#[error("Wrong package type {0} for platform {1}")]
InvalidPackageType(String, String),
/// Bundle type symbol missing in binary
#[error("__TAURI_BUNDLE_TYPE variable not found in binary. Make sure tauri crate and tauri-cli are up to date")]
MissingBundleTypeVar,
/// Failed to write binary file changed
#[error("Failed to write binary file changes: `{0}`")]
BinaryWriteError(String),
/// Invalid offset while patching binary file
#[error("Invalid offset while patching binary file")]
BinaryOffsetOutOfRange,
/// Unsupported architecture.
#[error("Architecture Error: `{0}`")]
ArchError(String),
Expand Down
Loading
Loading