Files
2026-11-01 12:58:01 +03:00

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('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&apos;")
}
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");
}