#![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")]
#![warn(missing_docs)]
#[cfg(not(feature = "default"))]
compile_error!(
"The feature `default` must be enabled to ensure \
forward compatibility with future version of this crate"
);
use std::collections::HashMap;
use std::env;
use std::io::{BufWriter, Write};
use std::path::Path;
use i_slint_compiler::diagnostics::BuildDiagnostics;
#[derive(Clone)]
pub struct CompilerConfiguration {
config: i_slint_compiler::CompilerConfiguration,
}
#[derive(Clone, PartialEq)]
pub enum EmbedResourcesKind {
AsAbsolutePath,
EmbedFiles,
EmbedForSoftwareRenderer,
}
impl Default for CompilerConfiguration {
fn default() -> Self {
Self {
config: i_slint_compiler::CompilerConfiguration::new(
i_slint_compiler::generator::OutputFormat::Rust,
),
}
}
}
impl CompilerConfiguration {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_include_paths(self, include_paths: Vec<std::path::PathBuf>) -> Self {
let mut config = self.config;
config.include_paths = include_paths;
Self { config }
}
#[must_use]
pub fn with_library_paths(self, library_paths: HashMap<String, std::path::PathBuf>) -> Self {
let mut config = self.config;
config.library_paths = library_paths;
Self { config }
}
#[must_use]
pub fn with_style(self, style: String) -> Self {
let mut config = self.config;
config.style = Some(style);
Self { config }
}
#[must_use]
pub fn embed_resources(self, kind: EmbedResourcesKind) -> Self {
let mut config = self.config;
config.embed_resources = match kind {
EmbedResourcesKind::AsAbsolutePath => {
i_slint_compiler::EmbedResourcesKind::OnlyBuiltinResources
}
EmbedResourcesKind::EmbedFiles => {
i_slint_compiler::EmbedResourcesKind::EmbedAllResources
}
EmbedResourcesKind::EmbedForSoftwareRenderer => {
i_slint_compiler::EmbedResourcesKind::EmbedTextures
}
};
Self { config }
}
#[must_use]
pub fn with_scale_factor(self, factor: f32) -> Self {
let mut config = self.config;
config.const_scale_factor = factor as f64;
Self { config }
}
#[must_use]
pub fn with_bundled_translations(
self,
path: impl Into<std::path::PathBuf>,
) -> CompilerConfiguration {
let mut config = self.config;
config.translation_path_bundle = Some(path.into());
Self { config }
}
#[doc(hidden)]
#[must_use]
pub fn with_debug_info(self, enable: bool) -> Self {
let mut config = self.config;
config.debug_info = enable;
Self { config }
}
#[cfg(feature = "sdf-fonts")]
#[must_use]
pub fn with_sdf_fonts(self, enable: bool) -> Self {
let mut config = self.config;
config.use_sdf_fonts = enable;
Self { config }
}
}
#[derive(derive_more::Error, derive_more::Display, Debug)]
#[non_exhaustive]
pub enum CompileError {
#[display("Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo.")]
NotRunViaCargo,
#[display("{_0:?}")]
CompileError(#[error(not(source))] Vec<String>),
#[display("Cannot write the generated file: {_0}")]
SaveError(std::io::Error),
}
struct CodeFormatter<Sink> {
indentation: usize,
in_string: bool,
in_char: usize,
escaped: bool,
sink: Sink,
}
impl<Sink> CodeFormatter<Sink> {
pub fn new(sink: Sink) -> Self {
Self { indentation: 0, in_string: false, in_char: 0, escaped: false, sink }
}
}
impl<Sink: Write> Write for CodeFormatter<Sink> {
fn write(&mut self, mut s: &[u8]) -> std::io::Result<usize> {
let len = s.len();
while let Some(idx) = s.iter().position(|c| match c {
b'{' if !self.in_string && self.in_char == 0 => {
self.indentation += 1;
true
}
b'}' if !self.in_string && self.in_char == 0 => {
self.indentation -= 1;
true
}
b';' if !self.in_string && self.in_char == 0 => true,
b'"' if !self.in_string && self.in_char == 0 => {
self.in_string = true;
self.escaped = false;
false
}
b'"' if self.in_string && !self.escaped => {
self.in_string = false;
false
}
b'\'' if !self.in_string && self.in_char == 0 => {
self.in_char = 1;
self.escaped = false;
false
}
b'\'' if !self.in_string && self.in_char > 0 && !self.escaped => {
self.in_char = 0;
false
}
b' ' | b'>' if self.in_char > 2 && !self.escaped => {
self.in_char = 0;
false
}
b'\\' if (self.in_string || self.in_char > 0) && !self.escaped => {
self.escaped = true;
false
}
_ if self.in_char > 0 => {
self.in_char += 1;
self.escaped = false;
false
}
_ => {
self.escaped = false;
false
}
}) {
let idx = idx + 1;
self.sink.write_all(&s[..idx])?;
self.sink.write_all(b"\n")?;
for _ in 0..self.indentation {
self.sink.write_all(b" ")?;
}
s = &s[idx..];
}
self.sink.write_all(s)?;
Ok(len)
}
fn flush(&mut self) -> std::io::Result<()> {
self.sink.flush()
}
}
#[test]
fn formatter_test() {
fn format_code(code: &str) -> String {
let mut res = Vec::new();
let mut formatter = CodeFormatter::new(&mut res);
formatter.write_all(code.as_bytes()).unwrap();
String::from_utf8(res).unwrap()
}
assert_eq!(
format_code("fn main() { if ';' == '}' { return \";\"; } else { panic!() } }"),
r#"fn main() {
if ';' == '}' {
return ";";
}
else {
panic!() }
}
"#
);
assert_eq!(
format_code(r#"fn xx<'lt>(foo: &'lt str) { println!("{}", '\u{f700}'); return Ok(()); }"#),
r#"fn xx<'lt>(foo: &'lt str) {
println!("{}", '\u{f700}');
return Ok(());
}
"#
);
assert_eq!(
format_code(r#"fn main() { ""; "'"; "\""; "{}"; "\\"; "\\\""; }"#),
r#"fn main() {
"";
"'";
"\"";
"{}";
"\\";
"\\\"";
}
"#
);
assert_eq!(
format_code(r#"fn main() { '"'; '\''; '{'; '}'; '\\'; }"#),
r#"fn main() {
'"';
'\'';
'{';
'}';
'\\';
}
"#
);
}
pub fn compile(path: impl AsRef<std::path::Path>) -> Result<(), CompileError> {
compile_with_config(path, CompilerConfiguration::default())
}
pub fn compile_with_config(
relative_slint_file_path: impl AsRef<std::path::Path>,
config: CompilerConfiguration,
) -> Result<(), CompileError> {
let path = Path::new(&env::var_os("CARGO_MANIFEST_DIR").ok_or(CompileError::NotRunViaCargo)?)
.join(relative_slint_file_path.as_ref());
let absolute_rust_output_file_path =
Path::new(&env::var_os("OUT_DIR").ok_or(CompileError::NotRunViaCargo)?).join(
path.file_stem()
.map(Path::new)
.unwrap_or_else(|| Path::new("slint_out"))
.with_extension("rs"),
);
let paths_dependencies =
compile_with_output_path(path, absolute_rust_output_file_path.clone(), config)?;
for path_dependency in paths_dependencies {
println!("cargo:rerun-if-changed={}", path_dependency.display());
}
println!("cargo:rerun-if-env-changed=SLINT_STYLE");
println!("cargo:rerun-if-env-changed=SLINT_FONT_SIZES");
println!("cargo:rerun-if-env-changed=SLINT_SCALE_FACTOR");
println!("cargo:rerun-if-env-changed=SLINT_ASSET_SECTION");
println!("cargo:rerun-if-env-changed=SLINT_EMBED_RESOURCES");
println!("cargo:rerun-if-env-changed=SLINT_EMIT_DEBUG_INFO");
println!("cargo:rerun-if-env-changed=SLINT_LIVE_PREVIEW");
println!(
"cargo:rustc-env=SLINT_INCLUDE_GENERATED={}",
absolute_rust_output_file_path.display()
);
Ok(())
}
pub fn compile_with_output_path(
input_slint_file_path: impl AsRef<std::path::Path>,
output_rust_file_path: impl AsRef<std::path::Path>,
config: CompilerConfiguration,
) -> Result<Vec<std::path::PathBuf>, CompileError> {
let mut diag = BuildDiagnostics::default();
let syntax_node = i_slint_compiler::parser::parse_file(&input_slint_file_path, &mut diag);
if diag.has_errors() {
let vec = diag.to_string_vec();
diag.print();
return Err(CompileError::CompileError(vec));
}
let mut compiler_config = config.config;
compiler_config.translation_domain = std::env::var("CARGO_PKG_NAME").ok();
let syntax_node = syntax_node.expect("diags contained no compilation errors");
let (doc, diag, loader) =
spin_on::spin_on(i_slint_compiler::compile_syntax_node(syntax_node, diag, compiler_config));
if diag.has_errors()
|| (!diag.is_empty() && std::env::var("SLINT_COMPILER_DENY_WARNINGS").is_ok())
{
let vec = diag.to_string_vec();
diag.print();
return Err(CompileError::CompileError(vec));
}
let output_file =
std::fs::File::create(&output_rust_file_path).map_err(CompileError::SaveError)?;
let mut code_formatter = CodeFormatter::new(BufWriter::new(output_file));
let generated = i_slint_compiler::generator::rust::generate(&doc, &loader.compiler_config)
.map_err(|e| CompileError::CompileError(vec![e.to_string()]))?;
let mut dependencies: Vec<std::path::PathBuf> = Vec::new();
for x in &diag.all_loaded_files {
if x.is_absolute() {
dependencies.push(x.clone());
}
}
diag.diagnostics_as_string().lines().for_each(|w| {
if !w.is_empty() {
println!("cargo:warning={}", w.strip_prefix("warning: ").unwrap_or(w))
}
});
write!(code_formatter, "{generated}").map_err(CompileError::SaveError)?;
dependencies.push(input_slint_file_path.as_ref().to_path_buf());
for resource in doc.embedded_file_resources.borrow().keys() {
if !resource.starts_with("builtin:") {
dependencies.push(Path::new(resource).to_path_buf());
}
}
code_formatter.sink.flush().map_err(CompileError::SaveError)?;
Ok(dependencies)
}
pub fn print_rustc_flags() -> std::io::Result<()> {
if let Some(board_config_path) =
std::env::var_os("DEP_MCU_BOARD_SUPPORT_BOARD_CONFIG_PATH").map(std::path::PathBuf::from)
{
let config = std::fs::read_to_string(board_config_path.as_path())?;
let toml = config.parse::<toml_edit::DocumentMut>().expect("invalid board config toml");
for link_arg in
toml.get("link_args").and_then(toml_edit::Item::as_array).into_iter().flatten()
{
if let Some(option) = link_arg.as_str() {
println!("cargo:rustc-link-arg={option}");
}
}
for link_search_path in
toml.get("link_search_path").and_then(toml_edit::Item::as_array).into_iter().flatten()
{
if let Some(mut path) = link_search_path.as_str().map(std::path::PathBuf::from) {
if path.is_relative() {
path = board_config_path.parent().unwrap().join(path);
}
println!("cargo:rustc-link-search={}", path.to_string_lossy());
}
}
println!("cargo:rerun-if-env-changed=DEP_MCU_BOARD_SUPPORT_MCU_BOARD_CONFIG_PATH");
println!("cargo:rerun-if-changed={}", board_config_path.display());
}
Ok(())
}