use std::path::{Path, PathBuf};
use std::process::Command;
#[allow(dead_code)]
mod renderer_dir {
include!("build/renderer_dir.rs");
}
#[allow(dead_code)]
mod windows_crt {
include!("build/windows_crt.rs");
}
use windows_crt::{
WINDOWS_CRT_ENV, WindowsCrt, blank_runtime_directives, find_msvc_library,
unreconcilable_message, windows_crt,
};
use renderer_dir::{
CACHE_DIR_ENV, FetchError, RENDERER_DIR_ENV, RendererKind, RendererLinkage, RendererSource,
Request, acquire_renderer, artifact_target, default_cache_root, renderer_linkage,
};
const MACOS_STATIC_ENV: &str = "DXC_MACOS_NATIVE_LIB";
const LINUX_STATIC_ENV: &str = "DXC_LINUX_NATIVE_LIB";
const DOCS_RS_ENV: &str = "DOCS_RS";
const ANDROID_KOTLIN_DIR_ENV: &str = "COMPOSE_RUST_ANDROID_KOTLIN_DIR";
const ANDROID_KOTLIN_DIR: &str = "android-kotlin";
const ANDROID_PACKAGE_ENV: &str = "COMPOSE_RUST_ANDROID_PACKAGE";
const DX_KOTLIN_DIR_ENV: &str = "WRY_ANDROID_KOTLIN_FILES_OUT_DIR";
const DX_PACKAGE_ENV: &str = "WRY_ANDROID_PACKAGE";
const DX_LIBRARY_ENV: &str = "WRY_ANDROID_LIBRARY";
fn main() {
println!("cargo:rustc-check-cfg=cfg(renderer_linked)");
println!("cargo:rustc-check-cfg=cfg(windows_crt_linked_in)");
println!("cargo:rustc-check-cfg=cfg(windows_crt_dynamic)");
println!("cargo:rerun-if-env-changed={RENDERER_DIR_ENV}");
println!("cargo:rerun-if-env-changed={MACOS_STATIC_ENV}");
println!("cargo:rerun-if-env-changed={LINUX_STATIC_ENV}");
println!("cargo:rerun-if-env-changed={CACHE_DIR_ENV}");
println!("cargo:rerun-if-env-changed=DIOXUS_COMPOSE_RENDERER_DIR");
println!("cargo:rerun-if-env-changed=DIOXUS_COMPOSE_CACHE_DIR");
println!("cargo:rerun-if-env-changed={DOCS_RS_ENV}");
println!("cargo:rerun-if-changed=build/renderer_dir.rs");
println!("cargo:rerun-if-changed=build/sha256.rs");
println!("cargo:rerun-if-changed=build/elf.rs");
println!("cargo:rerun-if-changed=build/windows_crt.rs");
if std::env::var_os("CARGO_FEATURE_NATIVE_RENDERER").is_none() {
return;
}
if std::env::var_os("CARGO_FEATURE_MOCK_RENDERER").is_some() {
return;
}
let target_family =
std::env::var("CARGO_CFG_TARGET_FAMILY").expect("Cargo sets CARGO_CFG_TARGET_FAMILY");
let target_os = std::env::var("CARGO_CFG_TARGET_OS").expect("Cargo sets CARGO_CFG_TARGET_OS");
match renderer_linkage(&target_os, &target_family, false) {
RendererLinkage::Installed | RendererLinkage::None => {
if target_os == "android" {
unpack_android_kotlin();
}
return;
}
RendererLinkage::Provided => {
println!("cargo:rustc-cfg=renderer_linked");
if target_os == "ios" {
println!("cargo:rustc-link-arg-cdylib=-Wl,-undefined,dynamic_lookup");
}
return;
}
RendererLinkage::Linked => {}
}
if target_os == "macos" {
println!("cargo:rustc-link-arg-cdylib=-Wl,-undefined,dynamic_lookup");
}
if std::env::var_os(DOCS_RS_ENV).is_some() {
return;
}
if target_os == "windows" {
if let Some(dir) = std::env::var_os("DXC_WINDOWS_NATIVE_LIB") {
link_windows_native(&PathBuf::from(dir));
return;
}
}
let manifest_dir = PathBuf::from(
std::env::var_os("CARGO_MANIFEST_DIR").expect("Cargo sets CARGO_MANIFEST_DIR"),
);
let crate_version = std::env::var("CARGO_PKG_VERSION").expect("Cargo sets CARGO_PKG_VERSION");
let target_arch =
std::env::var("CARGO_CFG_TARGET_ARCH").expect("Cargo sets CARGO_CFG_TARGET_ARCH");
let target = artifact_target(&target_os, &target_arch);
let static_override = match target_os.as_str() {
"macos" => std::env::var_os(MACOS_STATIC_ENV),
"linux" => std::env::var_os(LINUX_STATIC_ENV),
_ => None,
};
if let Some(dir) = static_override {
let dir = renderer_dir::absolute(&PathBuf::from(dir));
if !dir
.join(renderer_dir::renderer_static_file(&target_os))
.is_file()
{
panic!(
"\n\ncompose-rust: {} does not hold {}.\n\nThe variable names the \
directory a static renderer build wrote: \
renderer/desktop/scripts/build-macos.sh writes \
renderer/build/macos, and build-linux.sh writes \
renderer/build/linux. Unset it and this build finds or \
downloads a renderer by itself.\n\n",
dir.display(),
renderer_dir::renderer_static_file(&target_os)
);
}
check_schema_agreement(&dir);
link_static(&target_os, &target, &dir);
return;
}
let renderer_build = manifest_dir.join("../renderer/build");
let static_dir = match (target_os.as_str(), target_arch.as_str()) {
("macos", _) => Some(renderer_build.join("macos")),
("linux", "x86_64") => Some(renderer_build.join("linux")),
("linux", "aarch64") => Some(renderer_build.join("linux-arm64")),
_ => None,
};
let shared_dir = renderer_build.join("native-image/dist/lib");
let workspace_dirs: Vec<&Path> = static_dir
.as_deref()
.into_iter()
.chain(std::iter::once(shared_dir.as_path()))
.collect();
let cache_root = cache_root();
let renderer = match acquire_renderer(&Request {
env_dir: renamed_var(RENDERER_DIR_ENV, "DIOXUS_COMPOSE_RENDERER_DIR")
.map(PathBuf::from)
.as_deref(),
workspace_dirs: &workspace_dirs,
cache_root: cache_root.as_deref(),
crate_version: &crate_version,
target: &target,
target_os: &target_os,
fetch: Some(&fetch),
}) {
Ok(renderer) => renderer,
Err(message) => panic!("\n\n{message}\n\n"),
};
if matches!(
renderer.source,
RendererSource::Download | RendererSource::LocalArtifact
) {
println!(
"cargo:warning=compose-rust: unpacked the renderer for v{crate_version} ({target}) into {}",
renderer.lib_dir.display()
);
}
let lib_dir = renderer.lib_dir;
check_schema_agreement(&lib_dir);
match renderer.kind {
RendererKind::Static => link_static(&target_os, &target, &lib_dir),
RendererKind::Shared => link_shared(&target_os, &lib_dir),
}
}
struct StaticLink {
frameworks: &'static [&'static str],
libraries: &'static [&'static str],
arm64_libraries: &'static [&'static str],
search_paths: &'static [&'static str],
}
fn static_link(target_os: &str) -> Option<StaticLink> {
match target_os {
"macos" => Some(StaticLink {
frameworks: &[
"AppKit",
"Foundation",
"Metal",
"QuartzCore",
"CoreGraphics",
"CoreText",
"CoreServices",
"IOKit",
"Carbon",
"OpenGL",
"UserNotifications",
],
libraries: &["c++", "z"],
arm64_libraries: &[],
search_paths: &[],
}),
"linux" => Some(StaticLink {
frameworks: &[],
libraries: &["X11", "Xext", "GL", "fontconfig", "freetype", "stdc++", "z"],
arm64_libraries: &["EGL"],
search_paths: &[
"/usr/lib/x86_64-linux-gnu",
"/usr/lib/aarch64-linux-gnu",
"/usr/lib64",
],
}),
_ => None,
}
}
fn link_static(target_os: &str, target: &str, lib_dir: &Path) {
let Some(link) = static_link(target_os) else {
panic!(
"\n\n{}\n\n",
renderer_dir::no_static_link_message(target, lib_dir)
);
};
println!(
"cargo:rerun-if-changed={}",
lib_dir
.join(renderer_dir::renderer_static_file(target_os))
.display()
);
println!("cargo:rustc-link-search=native={}", lib_dir.display());
println!("cargo:rustc-link-lib=static=compose_rust_renderer");
for framework in link.frameworks {
println!("cargo:rustc-link-lib=framework={framework}");
}
for path in link.search_paths {
println!("cargo:rustc-link-search=native={path}");
}
let arm64 = target.ends_with("arm64") || target.ends_with("aarch64");
let extra: &[&str] = if arm64 { link.arm64_libraries } else { &[] };
for library in link.libraries.iter().chain(extra) {
println!("cargo:rustc-link-lib=dylib={library}");
}
println!("cargo:rustc-cfg=renderer_linked");
}
fn link_shared(target_os: &str, lib_dir: &Path) {
println!("cargo:rerun-if-changed={}", lib_dir.display());
println!("cargo:rustc-link-search=native={}", lib_dir.display());
println!("cargo:rustc-cfg=renderer_linked");
if target_os == "windows" {
println!("cargo:rustc-link-lib=dylib=libcompose_rust_renderer");
copy_renderer_beside_executables(lib_dir);
return;
}
println!("cargo:rustc-link-lib=dylib=compose_rust_renderer");
let export_dynamic = if target_os == "macos" {
"-Wl,-export_dynamic"
} else {
"-Wl,--export-dynamic"
};
println!("cargo:rustc-link-arg={export_dynamic}");
}
fn link_windows_native(dir: &Path) {
println!("cargo:rerun-if-env-changed=DXC_WINDOWS_NATIVE_LIB");
println!("cargo:rerun-if-env-changed={WINDOWS_CRT_ENV}");
println!("cargo:rerun-if-env-changed=LIB");
println!(
"cargo:rerun-if-changed={}",
dir.join("libcompose_rust_renderer.a").display()
);
for (piece, what) in [
("libcompose_rust_renderer.a", "the renderer"),
(
"native/dxc-windows-native.lib",
"the window and the MSVC bridge",
),
(
"native/dxc-windows-static-ucrt.lib",
"the bridge for a statically linked UCRT",
),
("gcc/libstdc++.a", "the GCC runtime the renderer carries"),
("skiko/skiko-bridges.lib", "skiko's C++ half"),
("skiko/skia.lib", "Skia"),
] {
if !dir.join(piece).is_file() {
panic!(
"\n\ncompose-rust: DXC_WINDOWS_NATIVE_LIB is {}, which has no {piece} ({what}).\n\n\
renderer/desktop/scripts/build-windows.sh writes the whole \
directory; build the renderer again, or point DXC_WINDOWS_NATIVE_LIB at the \
directory it wrote.\n\n",
dir.display()
);
}
}
let features = std::env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or_default();
let requested = std::env::var(WINDOWS_CRT_ENV).ok();
let crt = match windows_crt(&features, requested.as_deref()) {
Ok(crt) => crt,
Err(message) => panic!("\n\ncompose-rust: {message}\n\n"),
};
let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").expect("Cargo sets OUT_DIR"));
let rewritten = out_dir.join("windows-crt");
if let Err(error) = std::fs::create_dir_all(&rewritten) {
panic!(
"\n\ncompose-rust: could not make {}: {error}\n\n",
rewritten.display()
);
}
for sub in ["", "gcc", "native"] {
println!("cargo:rustc-link-search=native={}", dir.join(sub).display());
}
println!("cargo:rustc-link-search=native={}", rewritten.display());
println!("cargo:rustc-link-lib=static:+verbatim=libcompose_rust_renderer.a");
println!("cargo:rustc-link-lib=static:+whole-archive,+verbatim=dxc-windows-native.lib");
for library in ["libstdc++.a", "libgcc.a", "libgcc_eh.a", "libwinpthread.a"] {
println!("cargo:rustc-link-lib=static:+verbatim={library}");
}
for library in [
"skiko-bridges",
"skresources",
"skparagraph",
"skia",
"icu",
"jsonreader",
"skottie",
"svg",
"sksg",
"skshaper",
"skunicode_core",
"skunicode_icu",
"harfbuzz",
"skcms",
"libpng",
"zlib",
"libjpeg",
"libwebp",
"libwebp_sse41",
"wuffs",
"expat",
"d3d12allocator",
"spirv_cross",
"bentleyottmann",
] {
let file = format!("{library}.lib");
let source = dir.join("skiko").join(&file);
rewrite_runtime_directives(&source, &rewritten.join(&file));
println!("cargo:rustc-link-lib=static={library}");
}
let source = msvc_library("libcpmt.lib");
println!("cargo:rerun-if-changed={}", source.display());
rewrite_runtime_directives(&source, &rewritten.join("dxc-libcpmt.lib"));
println!("cargo:rustc-link-lib=static:+verbatim=dxc-libcpmt.lib");
match crt {
WindowsCrt::LinkedIn | WindowsCrt::FullyStatic => {
if crt == WindowsCrt::LinkedIn {
println!("cargo:rustc-cfg=windows_crt_linked_in");
} else {
println!(
"cargo:rustc-link-lib=static:+whole-archive,+verbatim=dxc-windows-static-ucrt.lib"
);
}
}
WindowsCrt::Dynamic => {
println!("cargo:rustc-cfg=windows_crt_dynamic");
}
}
for library in [
"user32",
"gdi32",
"ole32",
"oleaut32",
"imm32",
"uiautomationcore",
"d3d12",
"dxgi",
"d3dcompiler",
"dxguid",
"dwrite",
"usp10",
"fontsub",
"windowscodecs",
"opengl32",
"advapi32",
"shell32",
"bcrypt",
"runtimeobject",
] {
println!("cargo:rustc-link-lib=dylib={library}");
}
println!("cargo:rustc-link-lib=oldnames");
println!("cargo:rustc-link-lib=legacy_stdio_definitions");
println!("cargo:rustc-cfg=renderer_linked");
}
fn rewrite_runtime_directives(source: &Path, destination: &Path) {
let mut bytes = match std::fs::read(source) {
Ok(bytes) => bytes,
Err(error) => panic!(
"\n\ncompose-rust: could not read {}: {error}\n\n\
renderer/desktop/scripts/build-windows.sh writes every library the \
Windows renderer links; build the renderer again.\n\n",
source.display()
),
};
let found = blank_runtime_directives(&mut bytes);
if !found.unreconcilable.is_empty() {
panic!(
"\n\n{}\n\n",
unreconcilable_message(source, &found.unreconcilable)
);
}
if let Err(error) = std::fs::write(destination, &bytes) {
panic!(
"\n\ncompose-rust: could not write {}: {error}\n\n",
destination.display()
);
}
}
fn msvc_library(name: &str) -> PathBuf {
let mut tools = Vec::new();
if let Some(dir) = std::env::var_os("VCToolsInstallDir") {
tools.push(PathBuf::from(dir));
}
tools.extend(visual_studio_tools());
let mut xwin = Vec::new();
if let Some(dir) = std::env::var_os("XWIN_DIR") {
xwin.push(PathBuf::from(dir));
}
if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
let home = PathBuf::from(home);
xwin.push(home.join(".cache/cargo-xwin/xwin"));
xwin.push(home.join("Library/Caches/cargo-xwin/xwin"));
}
let lib = std::env::var("LIB").ok();
find_msvc_library(name, lib.as_deref(), &tools, &xwin).unwrap_or_else(|| {
panic!(
"\n\ncompose-rust: could not find {name}, the MSVC library the Windows renderer \
links the C++ standard library from.\n\n\
It is part of Visual Studio's C++ build tools (\"Desktop development with C++\"), \
which linking any Rust program for Windows already needs. It was looked for on \
LIB, under VCToolsInstallDir, in the newest Visual Studio vswhere reports, and \
where cargo-xwin keeps the MSVC libraries.\n\n"
)
})
}
fn visual_studio_tools() -> Vec<PathBuf> {
let vswhere = PathBuf::from(
std::env::var_os("ProgramFiles(x86)").unwrap_or_else(|| "C:\\Program Files (x86)".into()),
)
.join("Microsoft Visual Studio")
.join("Installer")
.join("vswhere.exe");
if !vswhere.is_file() {
return Vec::new();
}
let Ok(output) = Command::new(&vswhere)
.args([
"-latest",
"-products",
"*",
"-requires",
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
"-property",
"installationPath",
])
.output()
else {
return Vec::new();
};
let installation = String::from_utf8_lossy(&output.stdout).trim().to_string();
if installation.is_empty() {
return Vec::new();
}
let installation = PathBuf::from(installation);
let version_file = installation
.join("VC")
.join("Auxiliary")
.join("Build")
.join("Microsoft.VCToolsVersion.default.txt");
match std::fs::read_to_string(version_file) {
Ok(version) => vec![
installation
.join("VC")
.join("Tools")
.join("MSVC")
.join(version.trim()),
],
Err(_) => Vec::new(),
}
}
fn renamed_var(name: &str, old_name: &str) -> Option<std::ffi::OsString> {
if let Some(value) = std::env::var_os(name) {
return Some(value);
}
let value = std::env::var_os(old_name)?;
println!(
"cargo:warning=compose-rust: {old_name} is the old name of {name}; it is read for now and will stop being read in a later release"
);
Some(value)
}
fn cache_root() -> Option<PathBuf> {
if let Some(dir) = renamed_var(CACHE_DIR_ENV, "DIOXUS_COMPOSE_CACHE_DIR") {
return Some(PathBuf::from(dir));
}
default_cache_root(
std::env::var_os("HOME").map(PathBuf::from).as_deref(),
std::env::var_os("LOCALAPPDATA")
.map(PathBuf::from)
.as_deref(),
cfg!(windows),
)
}
fn fetch(url: &str, destination: &Path) -> Result<(), FetchError> {
match which("curl") {
true => curl(url, destination),
false if which("wget") => wget(url, destination),
false => Err(FetchError::Unreachable(
"neither curl nor wget is on PATH".to_string(),
)),
}
}
fn which(program: &str) -> bool {
let finder = if cfg!(windows) { "where" } else { "which" };
Command::new(finder)
.arg(program)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|status| status.success())
}
fn curl(url: &str, destination: &Path) -> Result<(), FetchError> {
let output = Command::new("curl")
.args(["--silent", "--show-error", "--location"])
.args(["--connect-timeout", "30", "--retry", "2"])
.arg("--output")
.arg(destination)
.args(["--write-out", "%{http_code}"])
.arg(url)
.output()
.map_err(|error| FetchError::Unreachable(format!("could not run curl: {error}")))?;
if !output.status.success() {
let detail = String::from_utf8_lossy(&output.stderr).trim().to_string();
let code = output.status.code().unwrap_or(-1);
return Err(if matches!(code, 6 | 7 | 28) {
FetchError::Unreachable(if detail.is_empty() {
format!("curl exited {code}")
} else {
detail
})
} else {
FetchError::Failed(format!("curl exited {code}: {detail}"))
});
}
match String::from_utf8_lossy(&output.stdout).trim() {
"200" => Ok(()),
"404" => Err(FetchError::NotFound),
code => Err(FetchError::Failed(format!("the server answered {code}"))),
}
}
fn wget(url: &str, destination: &Path) -> Result<(), FetchError> {
let output = Command::new("wget")
.args(["--quiet", "--timeout=30", "--tries=2"])
.arg("--output-document")
.arg(destination)
.arg(url)
.output()
.map_err(|error| FetchError::Unreachable(format!("could not run wget: {error}")))?;
if output.status.success() {
return Ok(());
}
let detail = String::from_utf8_lossy(&output.stderr).trim().to_string();
let code = output.status.code().unwrap_or(-1);
Err(match code {
4 => FetchError::Unreachable(format!("wget could not reach the network: {detail}")),
_ => FetchError::Failed(format!("wget exited {code}: {detail}")),
})
}
fn unpack_android_kotlin() {
println!("cargo:rerun-if-env-changed={ANDROID_KOTLIN_DIR_ENV}");
println!("cargo:rerun-if-env-changed={DX_KOTLIN_DIR_ENV}");
let Some((destination, package)) = android_gradle_kotlin() else {
println!(
"cargo:warning=compose-rust: neither {ANDROID_KOTLIN_DIR_ENV} nor \
{DX_KOTLIN_DIR_ENV} is set, so the renderer's Kotlin was not unpacked. An \
Android application needs it in its own source directory, because ART \
compiles it rather than us."
);
return;
};
let staged = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(ANDROID_KOTLIN_DIR);
if !staged.is_dir() {
panic!(
"\n\ncompose-rust: the Android Kotlin is missing from this copy of the \
crate.\n\nIt should be at {}. A checkout regenerates it with \
scripts/stage-android-kotlin.sh; a published crate carries it.\n\n",
staged.display()
);
}
println!("cargo:rerun-if-changed={}", staged.display());
if let Err(error) = copy_tree(&staged, &destination) {
panic!(
"\n\ncompose-rust: could not put the renderer's Kotlin into {}: {error}\n\n",
destination.display()
);
}
write_android_activity(&destination, package);
add_compose_to_gradle(&destination);
add_notification_permission(&destination);
}
fn add_notification_permission(destination: &Path) {
let Some(main_dir) = destination.parent() else {
return;
};
let manifest_path = main_dir.join("AndroidManifest.xml");
println!("cargo:rerun-if-changed={}", manifest_path.display());
let Ok(manifest) = std::fs::read_to_string(&manifest_path) else {
return;
};
let Some(text) = renderer_dir::with_notification_permission(&manifest) else {
return;
};
if let Err(error) = std::fs::write(&manifest_path, text) {
panic!(
"\n\ncompose-rust: could not declare the notification permission in {}: \
{error}\n\n",
manifest_path.display()
);
}
}
fn add_compose_to_gradle(destination: &Path) {
let Some(module_dir) = destination
.parent()
.and_then(Path::parent)
.and_then(Path::parent)
else {
return;
};
let module_path = module_dir.join("build.gradle.kts");
let Some(root_dir) = module_dir.parent() else {
return;
};
let root_path = root_dir.join("build.gradle.kts");
println!("cargo:rerun-if-changed={}", module_path.display());
println!("cargo:rerun-if-changed={}", root_path.display());
let (Ok(module), Ok(root)) = (
std::fs::read_to_string(&module_path),
std::fs::read_to_string(&root_path),
) else {
return;
};
let Some((module_text, root_text)) = renderer_dir::with_compose(&module, &root) else {
return;
};
for (path, text) in [(module_path, Some(module_text)), (root_path, root_text)] {
let Some(text) = text else { continue };
if let Err(error) = std::fs::write(&path, text) {
panic!(
"\n\ncompose-rust: could not put Compose into {}: {error}\n\n",
path.display()
);
}
}
}
fn android_gradle_kotlin() -> Option<(PathBuf, Option<String>)> {
println!("cargo:rerun-if-env-changed={ANDROID_PACKAGE_ENV}");
println!("cargo:rerun-if-env-changed={DX_PACKAGE_ENV}");
let read = |name: &str| std::env::var(name).ok();
renderer_dir::android_gradle_kotlin(
read(ANDROID_KOTLIN_DIR_ENV).as_deref(),
read(ANDROID_PACKAGE_ENV).as_deref(),
read(DX_KOTLIN_DIR_ENV).as_deref(),
read(DX_PACKAGE_ENV).as_deref(),
)
}
fn write_android_activity(destination: &Path, package: Option<String>) {
let Some(package) = package else {
return;
};
let mut path = destination.to_path_buf();
for component in package.split('.') {
path = path.join(component);
}
if let Err(error) = std::fs::create_dir_all(&path) {
panic!(
"\n\ncompose-rust: could not make {} for the generated Activity: \
{error}\n\n",
path.display()
);
}
let path = path.join("MainActivity.kt");
println!("cargo:rerun-if-env-changed={DX_LIBRARY_ENV}");
let library = std::env::var(DX_LIBRARY_ENV).unwrap_or_else(|_| "main".to_owned());
let source = format!(
r#"// Generated by compose-rust. DO NOT EDIT.
//
// The Android host. Kotlin owns the process and the frame loop: this Activity puts a
// ComposeView on screen and the interpreter draws the Host's tree inside it. Rust runs as
// a cdylib in the same process and never starts a loop of its own.
//
// The Host outlives the Activity, so a configuration change recomposes and nothing else.
package {package}
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import dev.darkpyonix.composerust.runtime.ComposeRustContent
import dev.darkpyonix.composerust.ui.platform.AndroidNotifications
import dev.darkpyonix.composerust.ui.platform.ComposeRustRuntime
import dev.darkpyonix.composerust.ui.platform.installSystemChrome
import dev.darkpyonix.composerust.ui.platform.installZoomLevelStore
class MainActivity : ComponentActivity() {{
// Registered before the Activity starts, which is the only time the platform allows.
// The prompt is shown when the application first posts or asks.
private val notificationPermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {{
AndroidNotifications.permissionAnswered()
}}
override fun onCreate(savedInstanceState: Bundle?) {{
// The window draws under the system's strips with nothing of the system's own
// over them, and the renderer decides whether the clock and the gesture bar are
// dark or light against what it drew.
installSystemChrome(this)
// Where the application's zoom level is kept between runs: its own preferences.
installZoomLevelStore(this)
super.onCreate(savedInstanceState)
ComposeRustRuntime.load("{library}")
// Before the Host: a press on a notification that started this process is in the
// intent, and it is the Host's first event once the Host exists.
AndroidNotifications.install(this) {{
notificationPermission.launch("android.permission.POST_NOTIFICATIONS")
}}
AndroidNotifications.handle(intent)
val host = ComposeRustRuntime.host()
ComposeRustRuntime.attached()
val view = ComposeView(this)
view.setContent {{
// The whole window. Where the system bars are is the renderer's to decide:
// a bar that opens the tree grows up into the status bar and a navigation
// grows down into the gesture bar, and whatever is left over is kept off the
// page there.
ComposeRustContent(host, Modifier.fillMaxSize())
}}
setContentView(view)
}}
override fun onStart() {{
super.onStart()
ComposeRustRuntime.start()
}}
override fun onStop() {{
ComposeRustRuntime.stop()
super.onStop()
}}
// A press on a notification while this Activity is already running.
override fun onNewIntent(intent: Intent) {{
super.onNewIntent(intent)
AndroidNotifications.handle(intent)
}}
override fun onDestroy() {{
// The process may stay without an Activity, and a worker's notification still has
// to go out from it.
ComposeRustRuntime.detached()
super.onDestroy()
}}
}}
"#
);
if let Err(error) = std::fs::write(&path, source) {
panic!(
"\n\ncompose-rust: could not write the Activity to {}: {error}\n\n",
path.display()
);
}
}
fn check_schema_agreement(lib_dir: &Path) {
let renderer_hash = read_hash(&lib_dir.join("..").join("schema-hash.txt"))
.or_else(|| read_hash(&lib_dir.join("schema-hash.txt")));
let ours = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("schema-hash.txt");
println!("cargo:rerun-if-changed={}", ours.display());
let crate_hash = read_hash(&ours);
match renderer_dir::schema_agreement(renderer_hash.as_deref(), crate_hash.as_deref()) {
renderer_dir::SchemaAgreement::Same => {}
renderer_dir::SchemaAgreement::Unknown => println!(
"cargo:warning=compose-rust: the renderer at {} does not say which schema it \
was built from, so this build cannot tell whether the two agree. If the \
window comes up empty, that is why: use a renderer built from this source \
tree, or set COMPOSE_RUST_RENDERER_DIR to one.",
lib_dir.display()
),
renderer_dir::SchemaAgreement::Different {
renderer,
crate_hash,
} => panic!(
"\n\ncompose-rust: this renderer was generated from a different schema than \
this crate.\n\n renderer: {renderer}\n crate: {crate_hash}\n\nA program \
built from the two would compile, start, open a window and draw nothing, \
because the first call across the boundary is refused.\n\nThe renderer at \
{} is the one to replace. A published renderer only matches the crate \
version it was published with, so a checkout whose schema has moved needs one \
built from that checkout: run the renderer build for this platform, or point \
COMPOSE_RUST_RENDERER_DIR at one that was.\n\n",
lib_dir.display()
),
}
}
fn read_hash(path: &Path) -> Option<String> {
std::fs::read_to_string(path)
.ok()
.map(|value| value.trim().to_owned())
.filter(|value| !value.is_empty())
}
fn copy_renderer_beside_executables(lib_dir: &Path) {
let Some(out_dir) = std::env::var_os("OUT_DIR") else {
return;
};
let out_dir = PathBuf::from(out_dir);
let Some(profile_dir) = out_dir
.parent()
.and_then(Path::parent)
.and_then(Path::parent)
else {
return;
};
for directory in [
profile_dir.to_path_buf(),
profile_dir.join("examples"),
profile_dir.join("deps"),
] {
copy_newer_files(lib_dir, &directory);
}
let Some(distribution) = lib_dir.parent() else {
return;
};
let mut roots = vec![profile_dir.to_path_buf()];
if let Some(target_dir) = profile_dir.parent() {
roots.push(target_dir.to_path_buf());
}
for root in roots {
for child in ["bin", "lib"] {
let source = distribution.join(child);
if source.is_dir() {
copy_newer_files(&source, &root.join(child));
}
}
}
}
fn copy_newer_files(from: &Path, to: &Path) {
let Ok(entries) = std::fs::read_dir(from) else {
return;
};
if std::fs::create_dir_all(to).is_err() {
return;
}
for entry in entries.flatten() {
let source = entry.path();
if !source.is_file() {
continue;
}
let destination = to.join(entry.file_name());
let newer = match (source.metadata(), destination.metadata()) {
(Ok(from), Ok(to)) => match (from.modified(), to.modified()) {
(Ok(from), Ok(to)) => from > to,
_ => true,
},
(Ok(_), Err(_)) => true,
_ => false,
};
if newer {
let _ = std::fs::copy(&source, &destination);
}
}
}
fn copy_tree(from: &Path, to: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(to)?;
for entry in std::fs::read_dir(from)? {
let entry = entry?;
let target = to.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_tree(&entry.path(), &target)?;
} else {
std::fs::copy(entry.path(), &target)?;
}
}
Ok(())
}