1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
fn copy_rs_files(
from: &std::path::Path,
to: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
for entry in std::fs::read_dir(from)? {
let entry = entry?;
let path = entry.path();
if path.extension().is_some_and(|e| e == "rs") {
std::fs::copy(&path, to.join(entry.file_name()))?;
}
}
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// NOTE: This probe assumes the crate lives exactly two directories below the
// monorepo root (i.e. <root>/client/rust/). If the crate is relocated,
// update these relative paths accordingly.
//
// Cached once so both the rerun-if-changed directives and the compile_protos
// calls use the same decision, preventing a future refactor from accidentally
// mismatching the two branches.
let in_monorepo = std::path::Path::new("../../pkg/api/submit.proto").exists();
// proto_root is the directory that contains k8s.io/, google/, etc.
// In the monorepo it is the repo-root proto/ (populated by `mage BootstrapProto`).
// Outside the monorepo it is proto/ relative to this crate (manually provided).
let proto_root = if in_monorepo { "../../proto" } else { "proto" };
// Without these directives Cargo's default is to re-run build.rs on every
// source change. Restrict re-runs to changes that actually affect codegen.
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=ARMADA_GENERATE");
// Only watch src/gen/ outside the monorepo (crates.io path). Cargo treats
// missing rerun-if-changed paths as "always rerun", so emitting this in a
// fresh monorepo clone (where src/gen/ never exists) would cause build.rs
// to re-run on every build.
if !in_monorepo {
println!("cargo:rerun-if-changed=src/gen/");
}
if in_monorepo {
// Narrow to the two subdirectories actually consumed by this build.
// Watching all of ../../proto/ would trigger rebuilds on unrelated
// proto changes elsewhere in the monorepo.
println!("cargo:rerun-if-changed=../../proto/k8s.io/");
println!("cargo:rerun-if-changed=../../proto/google/");
println!("cargo:rerun-if-changed=../../pkg/api/submit.proto");
println!("cargo:rerun-if-changed=../../pkg/api/event.proto");
println!("cargo:rerun-if-changed=../../pkg/api/job.proto");
println!("cargo:rerun-if-changed=../../pkg/api/health.proto");
} else {
// Outside the monorepo, expect third-party protos under proto/ relative
// to this crate (manually provided).
println!("cargo:rerun-if-changed=proto/");
}
let out_dir = std::env::var("OUT_DIR")?;
let out_path = std::path::Path::new(&out_dir);
let gen_dir = std::path::Path::new("src/gen");
// Pre-generated files exist (published crate from crates.io): copy them to
// OUT_DIR so that tonic::include_proto! resolves correctly, then return.
// This path requires no protoc and no proto sources.
// Guard on !in_monorepo so that monorepo contributors always compile from
// proto sources and never accidentally use stale pre-generated files left
// behind by a previous ARMADA_GENERATE=1 run.
if !in_monorepo && gen_dir.join("api.rs").exists() {
copy_rs_files(gen_dir, out_path)?;
return Ok(());
}
// Pass 1: compile k8s protos to generate their Rust types.
// No extern_path — we want these files emitted into OUT_DIR.
let k8s_protos: Vec<String> = [
"k8s.io/api/core/v1/generated.proto",
"k8s.io/api/networking/v1/generated.proto",
"k8s.io/apimachinery/pkg/api/resource/generated.proto",
"k8s.io/apimachinery/pkg/apis/meta/v1/generated.proto",
"k8s.io/apimachinery/pkg/runtime/generated.proto",
"k8s.io/apimachinery/pkg/util/intstr/generated.proto",
]
.iter()
.map(|p| format!("{proto_root}/{p}"))
.collect();
tonic_build::configure()
.build_client(false)
.build_server(false)
.compile_protos(&k8s_protos, &[proto_root])?;
// Pass 2: compile Armada API protos.
// extern_path tells prost that k8s types live in our module tree (from pass 1)
// rather than generating duplicate inline copies inside api.rs.
//
// When building inside the monorepo the protos live at ../../pkg/api/ relative
// to this manifest. When building from a crates.io package they are vendored
// into proto/pkg/api/ (mirroring the import paths used inside the .proto files).
//
// Input files are passed as include-relative names (e.g. "pkg/api/submit.proto")
// in the monorepo rather than physical paths (e.g. "../../pkg/api/submit.proto").
// This lets protoc resolve them through the include list, which avoids the
// "Input is shadowed" error that occurs when proto/pkg/api/ is also present on
// disk (e.g. during `cargo package` verification).
let armada_protos: Vec<String> = ["submit", "event", "job", "health"]
.iter()
.map(|name| {
if in_monorepo {
format!("pkg/api/{name}.proto")
} else {
format!("{proto_root}/pkg/api/{name}.proto")
}
})
.collect();
// Include order: proto_root is first so that google/ and k8s.io/ imports
// always resolve from the bootstrapped proto directory rather than anything
// else. "../../" is second (monorepo only) so that intra-Armada imports
// (e.g. `import "pkg/api/health.proto"`) fall through to the monorepo source.
let armada_includes: Vec<&str> = if in_monorepo {
vec![proto_root, "../../"]
} else {
vec![proto_root]
};
tonic_build::configure()
.build_server(false)
.extern_path(".k8s.io.api.core.v1", "crate::k8s::io::api::core::v1")
.extern_path(
".k8s.io.api.networking.v1",
"crate::k8s::io::api::networking::v1",
)
.extern_path(
".k8s.io.apimachinery.pkg.api.resource",
"crate::k8s::io::apimachinery::pkg::api::resource",
)
.extern_path(
".k8s.io.apimachinery.pkg.apis.meta.v1",
"crate::k8s::io::apimachinery::pkg::apis::meta::v1",
)
.extern_path(
".k8s.io.apimachinery.pkg.runtime",
"crate::k8s::io::apimachinery::pkg::runtime",
)
.extern_path(
".k8s.io.apimachinery.pkg.util.intstr",
"crate::k8s::io::apimachinery::pkg::util::intstr",
)
.compile_protos(&armada_protos, &armada_includes)?;
// When ARMADA_GENERATE is set (release CI), copy the generated files into
// src/gen/ so they can be bundled into the crates.io package. Users who
// download the crate then build without protoc via the early-return path above.
if std::env::var("ARMADA_GENERATE").is_ok() {
if gen_dir.exists() {
std::fs::remove_dir_all(gen_dir)?;
}
std::fs::create_dir_all(gen_dir)?;
copy_rs_files(out_path, gen_dir)?;
}
Ok(())
}