1
use sha2::{Digest, Sha256};
2
use std::env;
3
use std::fs;
4
use std::io::{self, Read};
5
use std::process::Command;
6

            
7
8
fn checksum(file_path: &str) -> io::Result<String> {
8
8
    let mut file = fs::File::open(file_path)?;
9
8
    let mut hasher = Sha256::new();
10
8
    let mut buffer = [0; 1024];
11

            
12
    loop {
13
68
        let bytes_read = file.read(&mut buffer)?;
14
68
        if bytes_read == 0 {
15
8
            break;
16
60
        }
17
60
        hasher.update(&buffer[..bytes_read]);
18
    }
19

            
20
8
    let hash = hasher.finalize();
21
256
    Ok(hash.iter().fold(String::new(), |mut acc, b| {
22
        use std::fmt::Write;
23
256
        write!(acc, "{b:02x}").unwrap();
24
256
        acc
25
256
    }))
26
8
}
27

            
28
/// Regenerate `doc/versions.org` from git history with git-cliff. It is a
29
/// build artifact (gitignored), `#+include`d by `disdoc.org`, so the disdoc
30
/// export below fails if it is missing. The pre-commit hook regenerates it the
31
/// same way before staging README; doing it here keeps a plain `cargo build`
32
/// self-contained (one build, no out-of-band step) instead of depending on a
33
/// hook having run.
34
2
fn regenerate_changelog() {
35
2
    let root = format!("{}/..", env::var("CARGO_MANIFEST_DIR").unwrap());
36
2
    let status = Command::new("git-cliff")
37
2
        .current_dir(&root)
38
2
        .args(["-o", "doc/versions.org"])
39
2
        .status()
40
2
        .expect(
41
2
            "failed to spawn git-cliff for doc/versions.org. cargo build requires \
42
2
             git-cliff on PATH (same as emacs); `cargo install git-cliff`",
43
        );
44
2
    assert!(status.success(), "git-cliff changelog generation failed");
45
2
}
46

            
47
2
fn main() {
48
    // `versions.org` is regenerated from every commit, so re-run when HEAD moves.
49
2
    println!("cargo:rerun-if-changed=../.git/HEAD");
50
2
    regenerate_changelog();
51

            
52
2
    println!("cargo:rerun-if-changed=../doc/disdoc.org");
53
2
    let status = Command::new("emacs")
54
2
        .args([
55
2
            "-q",
56
2
            "--batch",
57
2
            "--eval",
58
2
            "(progn (require 'ob-emacs-lisp) (require 'ob-shell) (setq org-confirm-babel-evaluate nil create-lockfiles nil))",
59
2
            "../doc/disdoc.org",
60
2
            "-l",
61
2
            "ox-md",
62
2
            "--eval",
63
2
            "(org-md-export-to-markdown)",
64
2
            "-f",
65
2
            "save-buffer",
66
2
            "-f",
67
2
            "kill-emacs",
68
2
        ])
69
2
        .status()
70
2
        .expect("Failed to export doc");
71

            
72
2
    assert!(status.success(), "Building finance doc failed");
73

            
74
    // OUT_DIR, not `../target`: the latter hardcodes the DEFAULT target directory
75
    // and breaks any build with CARGO_TARGET_DIR set. The previous version also
76
    // exported a fake CARGO_TARGET_DIR so lib.rs's include_str! could find the
77
    // file — that overrode a real one for this crate's whole compilation.
78
2
    let out_dir = env::var("OUT_DIR").expect("cargo always sets OUT_DIR for a build script");
79
2
    let doc_path = format!("{out_dir}/nomisync.md");
80
2
    fs::copy("../doc/disdoc.md", &doc_path).expect("Can't update finance doc file");
81
2
    println!("cargo:rustc-env=NOMISYNC_DOC={doc_path}");
82

            
83
2
    println!("cargo:rerun-if-changed=../doc/nomisync.org");
84
2
    let status = Command::new("emacs")
85
2
        .args([
86
2
            "-q",
87
2
            "--batch",
88
2
            "../doc/nomisync.org",
89
2
            "--eval",
90
2
            "(org-babel-do-load-languages 'org-babel-load-languages '((sql . t)))",
91
2
            "--eval",
92
2
            "(setq org-confirm-babel-evaluate nil create-lockfiles nil)",
93
2
            "--eval",
94
2
            "(org-babel-tangle)",
95
2
            "-f",
96
2
            "kill-emacs",
97
2
        ])
98
2
        .status()
99
2
        .expect("Failed to export SQL");
100

            
101
2
    assert!(status.success(), "Building SQL from org failed");
102

            
103
2
    sync_migration("../doc/nomisync.sql", "../migrations/0002_nomisync.sql");
104
2
    sync_migration(
105
2
        "../doc/0004_tags_canonical.sql",
106
2
        "../migrations/0004_tags_canonical.sql",
107
    );
108

            
109
2
    println!("cargo:rerun-if-changed=../migrations");
110
2
}
111

            
112
4
fn sync_migration(tangled: &str, migration: &str) {
113
4
    if fs::metadata(tangled).is_err() {
114
        return;
115
4
    }
116

            
117
    // No `--force`: removed in sqlfluff 4, where it is the default. The
118
    // repository's .sqlfluff handles RF04's false positive on `source`;
119
    // without that, `fix` cannot satisfy an unfixable rule, exits non-zero
120
    // forever, and the assert below stops the whole workspace building.
121
4
    let status = Command::new("sqlfluff")
122
4
        .args(["fix", "--dialect", "postgres", tangled])
123
4
        .status()
124
4
        .expect("Failed to format SQL");
125

            
126
4
    assert!(status.success(), "Formatting tangled migration failed");
127

            
128
4
    let needs_copy = match fs::metadata(migration) {
129
4
        Ok(_) => checksum(tangled).unwrap() != checksum(migration).unwrap(),
130
        Err(_) => true,
131
    };
132

            
133
4
    if needs_copy {
134
        fs::copy(tangled, migration).expect("Can't update migration file");
135
4
    } else {
136
4
        fs::remove_file(tangled).unwrap();
137
4
    }
138
4
}