295 lines
11 KiB
Rust
295 lines
11 KiB
Rust
use qt_build_utils::{QtBuild, QtTool};
|
|
use std::env;
|
|
use std::ffi::OsStr;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, Output};
|
|
|
|
fn run(command: &mut Command, description: &str) -> Output {
|
|
let output = command
|
|
.output()
|
|
.unwrap_or_else(|error| panic!("failed to run {description}: {error}"));
|
|
if !output.status.success() {
|
|
panic!(
|
|
"{description} failed: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
output
|
|
}
|
|
|
|
fn xml_escape(value: &str) -> String {
|
|
value
|
|
.replace('&', "&")
|
|
.replace('<', "<")
|
|
.replace('>', ">")
|
|
.replace('"', """)
|
|
.replace('\'', "'")
|
|
}
|
|
|
|
fn entries(dir: &str, extension: &str) -> Vec<PathBuf> {
|
|
let path = Path::new(dir);
|
|
println!("cargo:rerun-if-changed={dir}");
|
|
let mut files: Vec<_> = fs::read_dir(path)
|
|
.unwrap_or_else(|e| panic!("cannot read {dir}: {e}"))
|
|
.map(|entry| entry.expect("cannot read directory entry").path())
|
|
.filter(|path| path.is_file() && path.extension() == Some(OsStr::new(extension)))
|
|
.collect();
|
|
files.sort();
|
|
files
|
|
}
|
|
|
|
fn decode_png(path: &Path) -> (u32, u32, Vec<u8>) {
|
|
let file = fs::File::open(path)
|
|
.unwrap_or_else(|error| panic!("cannot open PNG {}: {error}", path.display()));
|
|
let mut decoder = png::Decoder::new(std::io::BufReader::new(file));
|
|
decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
|
|
let mut reader = decoder
|
|
.read_info()
|
|
.unwrap_or_else(|error| panic!("cannot decode PNG {}: {error}", path.display()));
|
|
let mut buffer = vec![
|
|
0;
|
|
reader
|
|
.output_buffer_size()
|
|
.expect("PNG dimensions exceed the decoder limit")
|
|
];
|
|
let info = reader
|
|
.next_frame(&mut buffer)
|
|
.unwrap_or_else(|error| panic!("cannot read PNG frame {}: {error}", path.display()));
|
|
buffer.truncate(info.buffer_size());
|
|
let pixels = match info.color_type {
|
|
png::ColorType::Rgba => buffer,
|
|
png::ColorType::Rgb => buffer
|
|
.chunks_exact(3)
|
|
.flat_map(|pixel| [pixel[0], pixel[1], pixel[2], 255])
|
|
.collect(),
|
|
png::ColorType::GrayscaleAlpha => buffer
|
|
.chunks_exact(2)
|
|
.flat_map(|pixel| [pixel[0], pixel[0], pixel[0], pixel[1]])
|
|
.collect(),
|
|
png::ColorType::Grayscale => buffer
|
|
.into_iter()
|
|
.flat_map(|value| [value, value, value, 255])
|
|
.collect(),
|
|
color => panic!("unsupported PNG color type {color:?} in {}", path.display()),
|
|
};
|
|
(info.width, info.height, pixels)
|
|
}
|
|
|
|
fn animation_frame(name: &str) -> Option<(&str, usize)> {
|
|
let frame = match name.as_bytes().last()? {
|
|
b'1' => 0,
|
|
b'2' => 1,
|
|
b'3' => 2,
|
|
_ => return None,
|
|
};
|
|
let stem = &name[..name.len() - 1];
|
|
for (direction, offset) in [("Up", 0), ("Down", 3), ("Left", 6), ("Right", 9)] {
|
|
if let Some(prefix) = stem.strip_suffix(direction) {
|
|
return Some((prefix, offset + frame));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn generate_assets(out: &Path, paths: &[PathBuf]) {
|
|
let mut generated = String::from("pub static IMAGES: &[ImageAsset] = &[\n");
|
|
let mut animations = std::collections::BTreeMap::<&str, [Option<usize>; 12]>::new();
|
|
let mut tray = None;
|
|
for (index, path) in paths.iter().enumerate() {
|
|
let name = path
|
|
.file_name()
|
|
.and_then(OsStr::to_str)
|
|
.expect("image asset filename is not UTF-8");
|
|
let (width, height, pixels) = decode_png(path);
|
|
let opaque_bottom = pixels
|
|
.chunks_exact(width as usize * 4)
|
|
.rposition(|row| row.chunks_exact(4).any(|pixel| pixel[3] != 0))
|
|
.map_or(0, |row| row + 1);
|
|
generated.push_str(&format!(
|
|
" ImageAsset {{ name: {name:?}, source: {:?}, width: {width}.0, height: {height}.0, opaque_bottom: {opaque_bottom}.0 }},\n",
|
|
format!("qrc:/Images/{name}")
|
|
));
|
|
if let Some((prefix, frame_index)) =
|
|
animation_frame(name.strip_suffix(".png").unwrap_or(name))
|
|
{
|
|
let frames = animations.entry(prefix).or_insert([None; 12]);
|
|
if frames[frame_index].replace(index).is_some() {
|
|
panic!("duplicate animation frame for {prefix}: {name}");
|
|
}
|
|
}
|
|
if name == "MadotsukiDown1.png" {
|
|
let argb = pixels
|
|
.chunks_exact(4)
|
|
.flat_map(|pixel| [pixel[3], pixel[0], pixel[1], pixel[2]])
|
|
.collect::<Vec<_>>();
|
|
tray = Some((width, height, argb));
|
|
}
|
|
}
|
|
generated.push_str("];\n");
|
|
generated.push_str(
|
|
"pub fn animation(prefix: &str) -> [&'static ImageAsset; 12] {\n match prefix {\n",
|
|
);
|
|
for (prefix, frames) in animations {
|
|
generated.push_str(&format!(" {prefix:?} => ["));
|
|
for (index, frame) in frames.into_iter().enumerate() {
|
|
let image_index = frame
|
|
.unwrap_or_else(|| panic!("animation {prefix} is missing frame at index {index}"));
|
|
generated.push_str(&format!("&IMAGES[{image_index}], "));
|
|
}
|
|
generated.push_str("],\n");
|
|
}
|
|
generated.push_str(" _ => panic!(\"unknown animation prefix: {prefix}\"),\n }\n}\n");
|
|
let (width, height, argb) = tray.expect("required tray PNG MadotsukiDown1.png is missing");
|
|
generated.push_str(&format!(
|
|
"pub const TRAY_ICON_WIDTH: i32 = {width};\npub const TRAY_ICON_HEIGHT: i32 = {height};\npub static TRAY_ICON_ARGB: &[u8] = &[\n"
|
|
));
|
|
for pixel in argb.chunks(16) {
|
|
generated.push_str(" ");
|
|
for byte in pixel {
|
|
generated.push_str(&format!("0x{byte:02X}, "));
|
|
}
|
|
generated.push('\n');
|
|
}
|
|
generated.push_str("];\n");
|
|
fs::write(out.join("assets_generated.rs"), generated)
|
|
.expect("cannot write generated image assets");
|
|
}
|
|
fn collect_include_paths(library: &pkg_config::Library, includes: &mut Vec<PathBuf>) {
|
|
for include in &library.include_paths {
|
|
if !includes.contains(include) {
|
|
includes.push(include.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let qt = QtBuild::new(vec!["Core".to_owned()]).expect("cannot find Qt installation");
|
|
let out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is not set"));
|
|
let image_paths = entries("assets/Images", "png");
|
|
generate_assets(&out, &image_paths);
|
|
let mut resources = Vec::new();
|
|
let sound_paths = entries("assets/Sounds", "wav");
|
|
for (paths, alias_dir) in [(&image_paths, "Images"), (&sound_paths, "Sounds")] {
|
|
for path in paths {
|
|
let name = path.file_name().unwrap().to_string_lossy();
|
|
println!("cargo:rerun-if-changed={}", path.display());
|
|
let absolute = path.canonicalize().expect("cannot resolve asset path");
|
|
resources.push(format!(
|
|
" <file alias=\"{}\">{}</file>",
|
|
xml_escape(&format!("{alias_dir}/{name}")),
|
|
xml_escape(&absolute.to_string_lossy())
|
|
));
|
|
}
|
|
}
|
|
for (path, alias) in [
|
|
("qml/Main.qml", "qml/Main.qml"),
|
|
("kwin/window-surfaces.js", "kwin/window-surfaces.js"),
|
|
] {
|
|
println!("cargo:rerun-if-changed={path}");
|
|
if !Path::new(path).is_file() {
|
|
panic!("required resource input missing: {path}");
|
|
}
|
|
let absolute = Path::new(path)
|
|
.canonicalize()
|
|
.expect("cannot resolve resource path");
|
|
resources.push(format!(
|
|
" <file alias=\"{}\">{}</file>",
|
|
xml_escape(alias),
|
|
xml_escape(&absolute.to_string_lossy())
|
|
));
|
|
}
|
|
let manifest = format!(
|
|
"<RCC>\n <qresource prefix=\"/\">\n{}\n </qresource>\n</RCC>\n",
|
|
resources.join("\n")
|
|
);
|
|
let qrc = out.join("yume4kde.qrc");
|
|
fs::write(&qrc, manifest).expect("cannot write resource manifest");
|
|
let rcc = qt
|
|
.installation()
|
|
.try_find_tool(QtTool::Rcc)
|
|
.expect("cannot find Qt rcc");
|
|
run(
|
|
Command::new(rcc)
|
|
.arg("--binary")
|
|
.arg(&qrc)
|
|
.arg("-o")
|
|
.arg(out.join("yume4kde.rcc")),
|
|
"Qt rcc",
|
|
);
|
|
println!(
|
|
"cargo:rustc-env=YUME4KDE_RESOURCE_PATH={}",
|
|
out.join("yume4kde.rcc").display()
|
|
);
|
|
|
|
let packages = [
|
|
"Qt6Core",
|
|
"Qt6Gui",
|
|
"Qt6Qml",
|
|
"Qt6Quick",
|
|
"Qt6DBus",
|
|
"Qt6Multimedia",
|
|
];
|
|
let mut includes = Vec::new();
|
|
for package in packages {
|
|
let library = pkg_config::Config::new()
|
|
.probe(package)
|
|
.unwrap_or_else(|e| panic!("required pkg-config package {package} unavailable: {e}"));
|
|
collect_include_paths(&library, &mut includes);
|
|
}
|
|
let configuration = out.join("native-dependencies");
|
|
run(
|
|
Command::new("cmake")
|
|
.args(["-S", "native", "-B"])
|
|
.arg(&configuration),
|
|
"LayerShellQt CMake configuration",
|
|
);
|
|
let metadata = fs::read_to_string(configuration.join("layer-shell-qt.txt"))
|
|
.expect("cannot read LayerShellQt target metadata");
|
|
let mut lines = metadata.lines();
|
|
includes.extend(
|
|
lines
|
|
.next()
|
|
.expect("missing LayerShellQt include paths")
|
|
.split(';')
|
|
.filter(|path| !path.is_empty())
|
|
.map(PathBuf::from),
|
|
);
|
|
let library_directory = lines
|
|
.next()
|
|
.expect("missing LayerShellQt library directory");
|
|
println!("cargo:rustc-link-search=native={library_directory}");
|
|
println!("cargo:rustc-link-lib=LayerShellQtInterface");
|
|
println!("cargo:rerun-if-changed=native/CMakeLists.txt");
|
|
println!("cargo:rerun-if-env-changed=CMAKE_PREFIX_PATH");
|
|
let moc = qt
|
|
.installation()
|
|
.try_find_tool(QtTool::Moc)
|
|
.expect("cannot find Qt moc");
|
|
let moc_output = out.join("moc_platform.cpp");
|
|
let mut moc_command = Command::new(moc);
|
|
moc_command
|
|
.arg("native/platform.hpp")
|
|
.arg("-o")
|
|
.arg(&moc_output);
|
|
for include in &includes {
|
|
moc_command.arg(format!("-I{}", include.display()));
|
|
}
|
|
run(&mut moc_command, "Qt moc");
|
|
println!("cargo:rerun-if-changed=native/platform.hpp");
|
|
println!("cargo:rerun-if-changed=native/platform.cpp");
|
|
println!("cargo:rerun-if-changed=src/main.rs");
|
|
let mut build = cxx_build::bridge("src/main.rs");
|
|
build
|
|
.file("native/platform.cpp")
|
|
.file(moc_output)
|
|
.include(".")
|
|
.std("c++17");
|
|
for include in includes {
|
|
build.include(include);
|
|
}
|
|
build
|
|
.flag_if_supported("-fPIC")
|
|
.compile("yume4kde-platform");
|
|
}
|