use std::path::{Path, PathBuf};
use std::process::Command;
#[allow(dead_code)]
mod renderer_dir {
include!("build/renderer_dir.rs");
}
use renderer_dir::{
CACHE_DIR_ENV, FetchError, RENDERER_DIR_ENV, RendererLinkage, RendererSource, Request,
acquire_renderer, artifact_target, default_cache_root, renderer_linkage,
};
const HOST_EXPORTS_LIBRARY: &str = "libcompose_rust_host_exports.so";
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(renderer_host_exports)");
println!("cargo:rerun-if-env-changed={RENDERER_DIR_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");
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 == "macos" {
if let Some(dir) = std::env::var_os("DXC_MACOS_NATIVE_LIB") {
let dir = PathBuf::from(dir);
println!("cargo:rerun-if-env-changed=DXC_MACOS_NATIVE_LIB");
println!(
"cargo:rerun-if-changed={}",
dir.join("libcompose_rust_renderer.a").display()
);
println!("cargo:rustc-link-search=native={}", dir.display());
println!("cargo:rustc-link-lib=static=compose_rust_renderer");
for framework in [
"AppKit",
"Foundation",
"Metal",
"QuartzCore",
"CoreGraphics",
"CoreText",
"CoreServices",
"IOKit",
"Carbon",
"OpenGL",
] {
println!("cargo:rustc-link-lib=framework={framework}");
}
println!("cargo:rustc-link-lib=dylib=c++");
println!("cargo:rustc-link-lib=dylib=z");
println!("cargo:rustc-cfg=renderer_linked");
return;
}
}
if target_os == "linux" {
if let Some(dir) = std::env::var_os("DXC_LINUX_NATIVE_LIB") {
let dir = PathBuf::from(dir);
println!("cargo:rerun-if-env-changed=DXC_LINUX_NATIVE_LIB");
println!(
"cargo:rerun-if-changed={}",
dir.join("libcompose_rust_renderer.a").display()
);
println!("cargo:rustc-link-search=native={}", dir.display());
println!("cargo:rustc-link-lib=static=compose_rust_renderer");
for library in ["X11", "Xext", "GL", "fontconfig", "freetype", "stdc++", "z"] {
println!("cargo:rustc-link-lib=dylib={library}");
}
for path in ["/usr/lib/x86_64-linux-gnu", "/usr/lib64"] {
println!("cargo:rustc-link-search=native={path}");
}
let exports = renderer_dir::absolute(&dir).join(HOST_EXPORTS_LIBRARY);
println!("cargo:rerun-if-changed={}", exports.display());
if !exports.is_file() {
panic!(
"\n\ncompose-rust: {} has the static renderer but not {HOST_EXPORTS_LIBRARY}.\n\n\
The renderer finds the Host's functions in the application's dynamic \
symbol table, and that library is what puts them there. Without it the \
application would build, open a black window and report that \
compose_rust_host_init is not in the image.\n\n\
renderer/desktop/scripts/build-linux.sh writes both \
files; build the renderer again, or point DXC_LINUX_NATIVE_LIB at the \
directory it wrote them to.\n\n",
dir.display()
);
}
let crate_version =
std::env::var("CARGO_PKG_VERSION").expect("Cargo sets CARGO_PKG_VERSION");
let target = artifact_target(
&target_os,
&std::env::var("CARGO_CFG_TARGET_ARCH").expect("Cargo sets CARGO_CFG_TARGET_ARCH"),
);
if let Err(message) = renderer_dir::name_after_its_location(
&exports,
&exports,
&target_os,
&crate_version,
&target,
) {
panic!("\n\n{message}\n\n");
}
println!("cargo:rustc-link-lib=dylib=compose_rust_host_exports");
println!("cargo:rustc-cfg=renderer_host_exports");
println!("cargo:rustc-cfg=renderer_linked");
return;
}
}
let manifest_dir = PathBuf::from(
std::env::var_os("CARGO_MANIFEST_DIR").expect("Cargo sets CARGO_MANIFEST_DIR"),
);
let workspace_lib_dir = manifest_dir.join("../renderer/build/native-image/dist/lib");
let crate_version = std::env::var("CARGO_PKG_VERSION").expect("Cargo sets CARGO_PKG_VERSION");
let target = artifact_target(
&target_os,
&std::env::var("CARGO_CFG_TARGET_ARCH").expect("Cargo sets CARGO_CFG_TARGET_ARCH"),
);
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_lib_dir: &workspace_lib_dir,
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);
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 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
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)
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(())
}