async_cffi_codegen/
codegen_build.rs

1use std::{
2    collections::HashMap,
3    fs,
4    io::{Error, ErrorKind, Result, Write as _},
5    path::Path,
6    process::{Command, Stdio},
7};
8
9use rs_schema::TraitSchema;
10
11use crate::{CodeGen as _, trait_to_async_cffi_py_schema, trait_to_async_cffi_rs_schema};
12
13pub fn generate_rs(
14    ios_trait_schema: &TraitSchema,
15    target_path: &Path,
16    supertrait_schemas: &HashMap<String, TraitSchema>,
17) -> Result<()> {
18    let file_schema = trait_to_async_cffi_rs_schema(ios_trait_schema, supertrait_schemas)
19        .map_err(|m| Error::new(ErrorKind::Other, m))?;
20    let new_contents = file_schema.codegen(0);
21    let formatted_new_contents = format_rust_code(&new_contents).unwrap_or_else(|err| {
22        println!("cargo:warning=Failed to format generated code: {err}");
23        new_contents
24    });
25    write_if_changed(&target_path, &formatted_new_contents)
26}
27
28fn format_rust_code(code: &str) -> Result<String> {
29    let edition = std::env::var("CARGO_PKG_EDITION").unwrap_or_else(|err| {
30        println!(
31            "cargo:warning=Using default edition 2024 for rustfmt (CARGO_PKG_EDITION missing: {err})"
32        );
33        "2024".to_string()
34    });
35
36    let mut process = Command::new("rustfmt")
37        .args(["--edition", &edition, "--emit", "stdout"])
38        .stdin(Stdio::piped())
39        .stdout(Stdio::piped())
40        .spawn()
41        .map_err(|e| Error::new(ErrorKind::Other, format!("failed to spawn rustfmt: {e}")))?;
42
43    if let Some(mut stdin) = process.stdin.take() {
44        stdin.write_all(code.as_bytes()).map_err(|e| {
45            Error::new(
46                ErrorKind::Other,
47                format!("failed to write to rustfmt stdin: {e}"),
48            )
49        })?;
50    }
51
52    let output = process.wait_with_output().map_err(|e| {
53        Error::new(
54            ErrorKind::Other,
55            format!("failed to read rustfmt output: {e}"),
56        )
57    })?;
58
59    if !output.status.success() {
60        return Err(Error::new(
61            ErrorKind::Other,
62            format!(
63                "rustfmt exited with status {}: {}",
64                output.status,
65                String::from_utf8_lossy(&output.stderr)
66            ),
67        ));
68    }
69
70    String::from_utf8(output.stdout).map_err(|e| {
71        Error::new(
72            ErrorKind::Other,
73            format!("rustfmt produced invalid UTF-8: {e}"),
74        )
75    })
76}
77
78fn format_python_code(code: &str) -> Result<String> {
79    let mut process = Command::new("python")
80        .args(["-m", "black", "-"])
81        .stdin(Stdio::piped())
82        .stdout(Stdio::piped())
83        .spawn()
84        .map_err(|e| Error::new(ErrorKind::Other, format!("failed to spawn black: {e}")))?;
85
86    if let Some(mut stdin) = process.stdin.take() {
87        stdin.write_all(code.as_bytes()).map_err(|e| {
88            Error::new(
89                ErrorKind::Other,
90                format!("failed to write to black stdin: {e}"),
91            )
92        })?;
93    }
94
95    let output = process.wait_with_output().map_err(|e| {
96        Error::new(
97            ErrorKind::Other,
98            format!("failed to read black output: {e}"),
99        )
100    })?;
101
102    if !output.status.success() {
103        return Err(Error::new(
104            ErrorKind::Other,
105            format!(
106                "black exited with status {}: {}",
107                output.status,
108                String::from_utf8_lossy(&output.stderr)
109            ),
110        ));
111    }
112
113    String::from_utf8(output.stdout).map_err(|e| {
114        Error::new(
115            ErrorKind::Other,
116            format!("black produced invalid UTF-8: {e}"),
117        )
118    })
119}
120
121pub fn generate_py(
122    ios_trait_schema: &TraitSchema,
123    target_path: &Path,
124    supertrait_schemas: &HashMap<String, TraitSchema>,
125) -> Result<()> {
126    let file_schema = trait_to_async_cffi_py_schema(ios_trait_schema, supertrait_schemas)
127        .map_err(|m| Error::new(ErrorKind::Other, m))?;
128
129    let new_contents = file_schema.codegen(0);
130    let formatted_new_contents = format_python_code(&new_contents).unwrap_or_else(|err| {
131        println!("cargo:warning=Failed to format generated Python code: {err}");
132        new_contents
133    });
134
135    write_if_changed(&target_path, &formatted_new_contents)
136}
137
138fn write_if_changed(target_path: &Path, contents: &str) -> Result<()> {
139    let normalized_new_contents = normalize_contents(contents);
140    let should_write = fs::read_to_string(target_path)
141        .map(|existing| normalize_contents(&existing) != normalized_new_contents)
142        .unwrap_or(true);
143
144    if should_write {
145        if let Some(parent) = target_path.parent() {
146            fs::create_dir_all(parent).map_err(|e| {
147                Error::new(
148                    ErrorKind::Other,
149                    format!(
150                        "failed to create parent directories for {}: {e}",
151                        parent.display()
152                    ),
153                )
154            })?;
155        }
156
157        let mut file = fs::File::create(target_path).map_err(|e| {
158            Error::new(
159                ErrorKind::Other,
160                format!("failed to create file {}: {e}", target_path.display()),
161            )
162        })?;
163        file.write_all(normalized_new_contents.as_bytes())
164            .map_err(|e| {
165                Error::new(
166                    ErrorKind::Other,
167                    format!("failed to write file {}: {e}", target_path.display()),
168                )
169            })?;
170        file.sync_all().map_err(|e| {
171            Error::new(
172                ErrorKind::Other,
173                format!("failed to sync file {}: {e}", target_path.display()),
174            )
175        })?;
176    }
177
178    Ok(())
179}
180
181fn normalize_contents(contents: &str) -> String {
182    let mut normalized = contents.replace("\r\n", "\n");
183    if !normalized.ends_with('\n') {
184        normalized.push('\n');
185    }
186
187    normalized
188}