gra/build/
mod.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3mod actions;
4mod desktop;
5mod flatpak;
6mod gresources;
7mod gsettings;
8mod i18n;
9mod makefile;
10
11use std::{
12    env::current_dir,
13    fs::{create_dir, create_dir_all, remove_dir_all, remove_file, File},
14    io::Write,
15    path::{Path, PathBuf},
16    process::Command,
17};
18
19use actions::*;
20use flatpak::*;
21use fs_extra::{dir::CopyOptions, file::read_to_string};
22use gresources::*;
23use gsettings::*;
24use i18n::*;
25use makefile::*;
26
27use crate::parse_project_descriptor;
28
29pub fn target_dir() -> PathBuf {
30    let target_env = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".into());
31    PathBuf::new().join(target_env)
32}
33
34pub fn build(project_dir: Option<&std::path::Path>, output_dir: Option<&std::path::Path>) {
35    let project_dir = project_dir
36        .map(PathBuf::from)
37        .unwrap_or_else(|| current_dir().unwrap());
38    let app_toml = project_dir.join("App.toml");
39    let cargo_toml = project_dir.join("Cargo.toml");
40
41    if !app_toml.exists() {
42        eprintln!("\n[gra] App.toml does not exist. Did you setup your project correctly? Run `cargo gra init` or check https://gitlab.com/floers/gtk-rust-app/-/tree/main#getting-started-tldr for more information.\n");
43        return;
44    }
45
46    let project_descriptor = parse_project_descriptor(&cargo_toml, &app_toml);
47
48    if project_descriptor.is_err() {
49        eprintln!(
50            "[gra] Could not parse App.toml: {}",
51            project_descriptor.unwrap_err()
52        );
53        return;
54    }
55
56    let project_descriptor = project_descriptor.unwrap();
57
58    println!("{:#?}", project_descriptor.app);
59
60    let gra_gen_dir = output_dir.unwrap_or_else(|| std::path::Path::new("target/gra-gen"));
61    std::fs::create_dir_all(gra_gen_dir).expect("Could not create out dir.");
62
63    build_actions(&project_descriptor, gra_gen_dir);
64    build_gschema_settings(&project_descriptor, gra_gen_dir);
65    build_flatpak(&project_dir, &project_descriptor, gra_gen_dir);
66    build_gresources(&project_descriptor, gra_gen_dir);
67    build_makefile(&project_descriptor, gra_gen_dir);
68    build_gettext(&project_dir, &project_descriptor);
69}
70
71/// Prepare the flatpak-temp directory which may be used to build a flatpak app.
72/// Returns a PathBuf to that directory.
73pub fn prepare_flatpak_temp(project_dir: &Path) -> Result<PathBuf, String> {
74    let target_dir = project_dir.join("target");
75    let flatpak_temp = target_dir.join("flatpak-temp");
76
77    // setup flatpak-temp dir
78    // let flatpak_temp = target_dir.join("flatpak-temp");
79    if flatpak_temp.exists() {
80        remove_dir_all(&flatpak_temp).map_err(|e| e.to_string())?;
81    }
82
83    println!("[gra] mkdir target/flatpak-temp");
84    create_dir_all(&flatpak_temp).map_err(|e| e.to_string())?;
85    println!("[gra] mkdir target/flatpak-temp/target");
86    create_dir_all(flatpak_temp.join("target")).map_err(|e| e.to_string())?;
87    println!("[gra] mkdir target/flatpak-temp/.cargo");
88    create_dir_all(flatpak_temp.join(".cargo")).map_err(|e| e.to_string())?;
89
90    let mut options = CopyOptions::new();
91    options.overwrite = true;
92    options.copy_inside = true;
93
94    println!("[gra] cp -r src target/flatpak-temp");
95    fs_extra::dir::copy(project_dir.join("src"), &flatpak_temp, &options)
96        .map_err(|e| e.to_string())?;
97    println!("[gra] cp -r po target/flatpak-temp");
98    fs_extra::dir::copy(project_dir.join("po"), &flatpak_temp, &options)
99        .map_err(|e| e.to_string())?;
100    println!("[gra] cp Cargo.toml target/flatpak-temp");
101    std::fs::copy(
102        project_dir.join("Cargo.toml"),
103        flatpak_temp.join("Cargo.toml"),
104    )
105    .map_err(|e| e.to_string())?;
106    println!("[gra] cp App.toml target/flatpak-temp");
107    std::fs::copy(project_dir.join("App.toml"), flatpak_temp.join("App.toml"))
108        .map_err(|e| e.to_string())?;
109    println!("[gra] cp -r target/gra-gen target/flatpak-temp/target");
110    fs_extra::dir::copy(
111        project_dir.join("target/gra-gen"),
112        flatpak_temp.join("target"),
113        &options,
114    )
115    .map_err(|e| e.to_string())?;
116
117    println!("[gra] Vendoring sources...");
118    let c = Command::new("cargo")
119        .current_dir(&flatpak_temp)
120        .args(["vendor", "target/vendor"])
121        .output()
122        .map_err(|e| e.to_string())?;
123
124    if let Ok(e) = String::from_utf8(c.stderr) {
125        if !e.trim().is_empty() {
126            println!("[gra] {}", e);
127        }
128    }
129    let mut config =
130        File::create(flatpak_temp.join(".cargo").join("config.toml")).map_err(|e| e.to_string())?;
131    config.write_all(&c.stdout).map_err(|e| e.to_string())?;
132
133    Ok(flatpak_temp)
134}
135
136pub fn generate(project_dir: &Path) {
137    let target_dir = project_dir.join("target");
138    let gra_gen = target_dir.join("gra-gen");
139    create_dir_all(&gra_gen).expect("Could not create target/gra-gen dir");
140    build(Some(project_dir), Some(gra_gen.as_path()));
141}
142
143pub fn clean(project_dir: &Path) {
144    let target_dir = project_dir.join("target");
145    let flatpak_temp = target_dir.join("flatpak-temp");
146    if flatpak_temp.exists() {
147        remove_dir_all(&flatpak_temp).expect("Could not clean flatpak-temp");
148    }
149    let flatpak_build = target_dir.join("flatpak-build");
150    if flatpak_build.exists() {
151        remove_dir_all(&flatpak_build).expect("Could not clean flatpak-build");
152    }
153    let gra_gen = target_dir.join("gra-gen");
154    if gra_gen.exists() {
155        remove_dir_all(&gra_gen).expect("Could not clean gra-gen");
156    }
157}
158
159const TEMPLATE: &str = include_str!("../../App.toml.template");
160const PO: &str = include_str!("../../po.template");
161
162pub fn init(project_dir: &Path) {
163    let app_toml = project_dir.join("App.toml");
164
165    if app_toml.exists() {
166        println!("[gra] App.toml already exists!");
167        return;
168    }
169    match File::create(app_toml) {
170        Ok(mut at) => {
171            if let Err(e) = writeln!(at, "{}", TEMPLATE) {
172                eprintln!("[gra] Failed to write to App.toml: {}", e);
173            }
174        }
175        Err(e) => {
176            eprintln!("Failed to create App.toml: {}", e)
177        }
178    }
179
180    let po_dir = project_dir.join("po");
181
182    if !po_dir.exists() {
183        if let Err(e) = create_dir("po") {
184            eprintln!("Failed to create po dir: {}", e);
185        }
186    }
187    let linguas_file = po_dir.join("LINGUAS");
188
189    if !linguas_file.exists() {
190        match File::create(linguas_file) {
191            Ok(mut f) => {
192                if let Err(e) = writeln!(f, "en") {
193                    eprintln!("[gra] Failed to write to App.toml: {}", e);
194                }
195            }
196            Err(e) => {
197                eprintln!("Failed to create App.toml: {}", e)
198            }
199        }
200    }
201
202    let en_file = po_dir.join("en.po");
203
204    if !en_file.exists() {
205        match File::create(en_file) {
206            Ok(mut f) => {
207                if let Err(e) = writeln!(f, "{}", PO) {
208                    eprintln!("[gra] Failed to write to App.toml: {}", e);
209                }
210            }
211            Err(e) => {
212                eprintln!("Failed to create App.toml: {}", e)
213            }
214        }
215    }
216}
217pub struct FlatpakArgs {
218    pub release: Option<String>,
219    pub prepare_only: bool,
220    pub arch: Option<String>,
221}
222
223pub fn flatpak(project_dir: &Path, arguments: FlatpakArgs) -> std::io::Result<()> {
224    let target_dir = project_dir.join("target");
225    println!("[gra] Prepare flatpak build...");
226
227    let project_descriptor = match parse_project_descriptor(
228        &project_dir.join("Cargo.toml"),
229        &project_dir.join("App.toml"),
230    ) {
231        Err(e) => {
232            eprintln!("[gra] Could not parse App.toml for flatpak build: {}", e);
233            return Err(e);
234        }
235        Ok(pd) => pd,
236    };
237
238    let gra_gen = target_dir.join("gra-gen");
239
240    if arguments.release.is_some() {
241        clean(project_dir);
242        generate(project_dir);
243
244        let flatpak_temp = match prepare_flatpak_temp(&PathBuf::from(target_dir.parent().unwrap()))
245        {
246            Ok(f) => f,
247            Err(e) => {
248                eprintln!("Could not prepare flatpak-temp: {:?}", e);
249                std::process::exit(-1);
250            }
251        };
252
253        let manifest_yml = target_dir.join(format!("{}.yml", &project_descriptor.app.id));
254
255        remove_file(gra_gen.join(format!("data/{}.dev.yml", &project_descriptor.app.id)))?;
256        if let Err(e) = std::fs::rename(
257            gra_gen.join(format!("data/{}.yml", &project_descriptor.app.id)),
258            &manifest_yml,
259        ) {
260            eprintln!(
261                "[gra] Could not move target/gra-gen/data/{0}.yml to target/{0}.yml: {1}",
262                &project_descriptor.app.id, e
263            );
264            return Err(e);
265        }
266
267        let tar_file = target_dir.join(format!("{}.tar.xz", &project_descriptor.package.name));
268
269        println!("[gra] Call tar -C {:?} -cJf {:?} .", flatpak_temp, tar_file);
270        match Command::new("tar")
271            .args([
272                "-C",
273                flatpak_temp.to_str().unwrap(),
274                "-cJf",
275                tar_file.to_str().unwrap(),
276                ".",
277            ])
278            .spawn()
279        {
280            Err(e) => {
281                eprintln!("[gra] tar flatpak-temp resulted in error: {}", e);
282                return Err(e);
283            }
284            Ok(mut c) => {
285                if let Err(e) = c.wait() {
286                    eprintln!("[gra] tar command failed: {}", e);
287                    return Err(e);
288                }
289            }
290        }
291        println!("[gra] Call sha256sum {:?}", &tar_file);
292
293        let sha = Command::new("sha256sum").arg(&tar_file).output();
294        if let Err(e) = sha {
295            eprintln!(
296                "[gra] sha256sum for {:?} resulted in error: {}",
297                &tar_file, e
298            );
299            return Err(e);
300        }
301
302        let sha = String::from_utf8_lossy(&sha.unwrap().stdout)
303            .to_string()
304            .split_once(' ')
305            .unwrap()
306            .0
307            .trim()
308            .to_string();
309
310        let manifest_template = read_to_string(&manifest_yml).map_err(from_fs_to_io)?;
311        let mut sources = format!(
312            r#"      - type: archive
313        url: "{}"
314        sha256: "{}""#,
315            arguments.release.as_ref().unwrap(),
316            &sha
317        );
318        if arguments.release.as_ref().unwrap() == "local" {
319            sources = format!(
320                r#"      - type: archive
321        path: "{}.tar.xz""#,
322                &project_descriptor.package.name,
323            );
324        }
325        let manifest = manifest_template.replace("{sources}", &sources);
326
327        let sha_file = target_dir.join(format!("{}.sha256.txt", &project_descriptor.package.name));
328
329        match File::create(&sha_file) {
330            Ok(mut f) => {
331                f.write_all(sha.as_bytes())?;
332            }
333            Err(e) => {
334                eprintln!("[gra] Could not create {:?}", sha_file);
335                return Err(e);
336            }
337        }
338
339        match File::create(&manifest_yml) {
340            Ok(mut f) => {
341                f.write_all(manifest.as_bytes())?;
342            }
343            Err(e) => {
344                eprintln!("[gra] Could not write to release {:?}", manifest_yml);
345                return Err(e);
346            }
347        }
348
349        println!(
350            "[gra] Created flatpak release file: {:?}, {}",
351            tar_file,
352            manifest_yml.to_string_lossy()
353        );
354
355        return Ok(());
356    }
357
358    let flatpak_temp = match prepare_flatpak_temp(project_dir) {
359        Ok(f) => f,
360        Err(e) => {
361            eprintln!("Could not prepare flatpak-temp: {:?}", e);
362            std::process::exit(-1);
363        }
364    };
365
366    println!("[gra] Setup temp flatpak build dir: {:?}.", flatpak_temp);
367
368    if arguments.prepare_only {
369        return Ok(());
370    }
371
372    // setup flatpak-build dir
373    let flatpak_build_rel = "../flatpak-build";
374
375    println!("[gra] Running flatpak-builder --repo={0}/repo {0}/host {1}/data/{2}.dev.yml --force-clean --state-dir={0}/state {3}", 
376        flatpak_build_rel,
377        gra_gen.to_str().unwrap(),
378        &project_descriptor.app.id,
379        arguments.arch.as_ref().map(|a| format!("--arch {}", a)).unwrap_or_else(|| "".into())
380    );
381
382    let mut c = Command::new("flatpak-builder");
383    c.current_dir(&flatpak_temp);
384    c.arg(format!("--repo={}/{}", flatpak_build_rel, "repo"));
385    c.arg("--force-clean");
386    c.arg(format!("--state-dir={}/{}", flatpak_build_rel, "state"));
387    if let Some(arch) = &arguments.arch.as_ref() {
388        c.arg("--arch").arg(arch);
389    }
390    if let Some(arch) = arguments.arch.as_ref() {
391        c.arg(format!("{}/{}", flatpak_build_rel, arch));
392    } else {
393        c.arg(format!("{}/{}", flatpak_build_rel, "host"));
394    }
395    c.arg(format!(
396        "../gra-gen/data/{}.dev.yml",
397        &project_descriptor.app.id
398    ));
399    let mut c = c.spawn()?;
400    c.wait()?;
401
402    println!(
403        "[gra] Bundling flatpak build-bundle {0}/repo {1}/{2}.flatpak {3}",
404        flatpak_build_rel,
405        target_dir.to_str().unwrap(),
406        &project_descriptor.package.name,
407        &project_descriptor.app.id
408    );
409
410    let flatpak_file_name = format!("{}.flatpak", &project_descriptor.package.name);
411
412    let mut c = Command::new("flatpak")
413        .current_dir(&flatpak_temp)
414        .arg("build-bundle")
415        .arg(format!("{}/repo", flatpak_build_rel))
416        .arg(format!("../{}", flatpak_file_name))
417        .arg(&project_descriptor.app.id)
418        .spawn()?;
419    c.wait()?;
420
421    if flatpak_temp.exists() {
422        if let Err(e) = remove_dir_all(&flatpak_temp) {
423            eprintln!("[gra] Could not clean flatpak-temp.");
424            return Err(e);
425        }
426    }
427
428    Ok(())
429}
430
431fn from_fs_to_io(e: fs_extra::error::Error) -> std::io::Error {
432    std::io::Error::new(
433        match &e.kind {
434            fs_extra::error::ErrorKind::NotFound => std::io::ErrorKind::NotFound,
435            fs_extra::error::ErrorKind::PermissionDenied => std::io::ErrorKind::PermissionDenied,
436            fs_extra::error::ErrorKind::AlreadyExists => std::io::ErrorKind::AlreadyExists,
437            fs_extra::error::ErrorKind::Interrupted => std::io::ErrorKind::Interrupted,
438            fs_extra::error::ErrorKind::InvalidFolder => std::io::ErrorKind::InvalidInput,
439            fs_extra::error::ErrorKind::InvalidFile => std::io::ErrorKind::InvalidInput,
440            fs_extra::error::ErrorKind::InvalidFileName => std::io::ErrorKind::InvalidInput,
441            fs_extra::error::ErrorKind::InvalidPath => std::io::ErrorKind::InvalidInput,
442            fs_extra::error::ErrorKind::Io(e) => e.kind(),
443            fs_extra::error::ErrorKind::StripPrefix(_) => std::io::ErrorKind::Other,
444            fs_extra::error::ErrorKind::OsString(_) => std::io::ErrorKind::Other,
445            fs_extra::error::ErrorKind::Other => std::io::ErrorKind::Other,
446        },
447        format!("{:?}", e),
448    )
449}