Skip to content

Commit

Permalink
Extract build specific logic to build crate, refactor crate
Browse files Browse the repository at this point in the history
  • Loading branch information
dastansam committed Aug 2, 2024
1 parent 8a73713 commit 32cf9f1
Show file tree
Hide file tree
Showing 16 changed files with 236 additions and 191 deletions.
13 changes: 8 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@ members = ["update_icons"]

[dependencies]
gtk = { version = "0.8", package = "gtk4" }

[build-dependencies]
gvdb = { version = "0.5.3", features = ["gresource"] }
serde = { version = "1.0.197", features = ["derive"] }
toml = "0.8.11"
gvdb = { version = "0.5.3", features = ["gresource"], optional = true }
serde = { version = "1.0.197", features = ["derive"], optional = true }
toml = { version = "0.8.11", optional = true }

[package.metadata.docs.rs]
all-features = true
Expand All @@ -38,3 +36,8 @@ all = []
# Enable entire icon kits
icon-development-kit = []
fluent-system-icons = []
build-utils = [
"gvdb",
"serde",
"toml",
]
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,39 @@ icon_folder = "my_svg_icons"

```toml
relm4-icons = "0.8.0"

[build-dependencies]
relm4-icons = { version = "0.8.0", features = ["build-utils"] }
```

### 4. Bundle the icons 📦

Create a module named `icon_names` in your crate like this:

```rust
mod icon_names {
include!(concat!(env!("OUT_DIR"), "/icon_names.rs"));
}
```

And in your `build.rs` file, use `relm4-icons` to bundle the icons and include them in the compiled binary:

```rust

fn main() {
let manifest_path = std::env::var("CARGO_MANIFEST_DIR").unwrap();

let config = Config::load(
&manifest_path,
Some(relm4_icons::constants::SHIPPED_ICONS_PATH.to_string()),
)
.expect("couldn't load manifest");

relm4_icons::build_utils::bundle_icons(config, &manifest_path);
}
```

### 4. Load the icons 🛫
### 5. Load the icons 🛫

Add this to your initialization code:

Expand All @@ -78,7 +108,7 @@ button.set_icon_name("plus");
You can also use the `icon_names` module for extra compile-time generated icon names.

```rust
use relm4_icons::icon_names;
use crate::icon_names;

let button = gtk::Button::default();
button.set_icon_name(icon_names::PLUS);
Expand Down
183 changes: 14 additions & 169 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,176 +1,21 @@
use std::{
collections::HashMap,
env,
ffi::OsStr,
io,
path::{Path, PathBuf},
};
//! Save constant names for icons and manifest path
use gvdb::gresource::{GResourceBuilder, GResourceFileData, PreprocessOptions};

const CONFIG_FILE: &str = "icons.toml";
const GENERAL_PREFIX: &str = "/org/gtkrs/icons/scalable/actions/";
const SHIPPED_ICONS_PATH: &str = "icons";

const TARGET_FILE: &str = "resources.gresource";
const CONSTANTS_FILE: &str = "icon_names.rs";

#[derive(Default, serde::Deserialize)]
struct Config {
app_id: Option<String>,
base_resource_path: Option<String>,
icon_folder: Option<String>,
icons: Option<Vec<String>>,
}

impl Config {
fn load(dir: &str) -> Result<Self, io::Error> {
let config_path: PathBuf = [dir, CONFIG_FILE].iter().collect();
let config_file = std::fs::read_to_string(config_path)?;
Ok(toml::from_str(&config_file).expect("Couldn't parse icon config file"))
}
}

fn path_to_icon_name(string: &OsStr) -> String {
match string.to_str() {
Some(string) => {
if string.ends_with(".svg") {
string
.trim_end_matches("-symbolic.svg")
.trim_end_matches(".svg")
.to_owned()
} else {
panic!("Found non-icon file `{string}`");
}
}
None => panic!("Failed to convert file name `{string:?}` to string"),
}
}
const CONSTANTS_FILE: &str = "constants.rs";

fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let mut manifest_dir = Path::new(&out_dir).canonicalize().unwrap();
eprintln!("Canonical manifest dir: {manifest_dir:?}");

let (config, config_dir) = if cfg!(docsrs) {
if let Ok(source_dir) = env::var("SOURCE_DIR") {
(Config::load(&source_dir).unwrap_or_default(), source_dir)
} else {
(Config::default(), "".into())
}
} else {
// Try finding the target directory which is just below the manifest directory
// of the user.
// Unfortunately, the CARGO_MANIFEST_DIR env var passed by cargo always points
// to this crate, so we wouldn't find the users config file this way.
while !manifest_dir.join("Cargo.toml").exists() {
if !manifest_dir.pop() {
panic!("Couldn't find your manifest directory");
}
}
let config_dir = manifest_dir
.to_str()
.expect("Couldn't convert manifest directory to string")
.to_owned();
(
Config::load(&config_dir).expect("Couldn't find `icons.toml` next to `Cargo.toml`"),
config_dir,
)
};
let out_dir = std::env::var("OUT_DIR").unwrap();

eprintln!("Canonical config dir: {config_dir:?}");
println!("cargo:rerun-if-changed={config_dir}/icons.toml");

let mut icons: HashMap<String, PathBuf> = HashMap::new();

if let Some(folder) = &config.icon_folder {
println!("cargo:rerun-if-changed={folder}");
let custom_icons_path: PathBuf = [&config_dir, folder].iter().collect();
let read_dir = std::fs::read_dir(custom_icons_path)
.expect("Couldn't open icon path specified in config (relative to the manifest)");
for entry in read_dir {
let entry = entry.unwrap();
let icon_name = path_to_icon_name(&entry.file_name());
if icons.insert(icon_name.clone(), entry.path()).is_some() {
panic!("Icon with name `{icon_name}` exists twice")
}
}
}

if let Some(icon_names) = config.icons {
let dirs =
std::fs::read_dir(SHIPPED_ICONS_PATH).expect("Couldn't open folder of shipped icons");
let dirs: Vec<_> = dirs
.map(|entry| {
let entry = entry.expect("Couldn't open directories in shipped icon folder");
entry.path()
})
.collect();

'outer: for icon_name in icon_names {
for dir in &dirs {
let icon_file_name = format!("{icon_name}-symbolic.svg");
let icon_path = dir.join(icon_file_name);
if icon_path.exists() {
if icons.insert(icon_name.clone(), icon_path).is_some() {
panic!("Icon with name `{icon_name}` exists twice")
}
continue 'outer;
}
}
panic!("Icon {icon_name} not found in shipped icons");
}
}

let prefix = if let Some(base_resource_path) = &config.base_resource_path {
format!("{}icons/scalable/actions/", base_resource_path)
} else if let Some(app_id) = &config.app_id {
format!("/{}/icons/scalable/actions/", app_id.replace('.', "/"))
} else {
GENERAL_PREFIX.into()
};

// Generate resource bundle
let resources = icons
.iter()
.map(|(icon, path)| {
GResourceFileData::from_file(
[&prefix, icon, "-symbolic.svg"].into_iter().collect(),
path,
true,
&PreprocessOptions::xml_stripblanks(),
)
.unwrap()
})
.collect();

let data = GResourceBuilder::from_file_data(resources)
.build()
.expect("Failed to build resource bundle");

std::fs::write(Path::new(&out_dir).join(TARGET_FILE), data).unwrap();
let manifest_path = std::env::var("CARGO_MANIFEST_DIR").unwrap();

// Create file that contains the icon names as constants
let constants: String = icons
.iter()
.map(|(icon_name, icon_path)| {
let const_name = icon_name.to_uppercase().replace('-', "_");
format!(
"
/// Icon name of the icon `{icon_name}`, found at `{icon_path:?}`.
pub const {const_name}: &str = \"{icon_name}\";
"
)
})
.chain([format!(
"pub(crate) const APP_ID: &str = \"{}\";",
config.app_id.unwrap_or_default()
)])
.chain([format!(
"pub(crate) const BASE_RESOURCE_PATH: &str = \"{}\";",
config.base_resource_path.unwrap_or_default()
)])
.collect();

std::fs::write(Path::new(&out_dir).join(CONSTANTS_FILE), constants).unwrap();
let constants = format!(
"pub const SHIPPED_ICONS_PATH: &str = \"{}/icons\";",
manifest_path
);

std::fs::write(
std::path::Path::new(&out_dir).join(CONSTANTS_FILE),
constants,
)
.unwrap();
}
Loading

0 comments on commit 32cf9f1

Please sign in to comment.