@@ -216,6 +216,114 @@ impl<'a> Downloader<'a> {
216216 ) -> Self {
217217 Self { log, output_dir, versions_dir }
218218 }
219+
220+ /// Build a binary from a git repository at a specific commit.
221+ ///
222+ /// This function:
223+ /// 1. Checks for cached binaries at `out/.build-cache/{project}/{commit}/`
224+ /// 2. If not cached, shallow clones the repo to a temp directory
225+ /// 3. Builds the specified binaries with cargo
226+ /// 4. Caches the built binaries
227+ /// 5. Returns paths to the cached binaries
228+ async fn build_from_git (
229+ & self ,
230+ project : & str ,
231+ commit : & str ,
232+ binaries : & [ ( & str , & [ & str ] ) ] , // (binary_name, cargo_args)
233+ ) -> Result < Vec < Utf8PathBuf > > {
234+ let cache_dir =
235+ self . output_dir . join ( ".build-cache" ) . join ( project) . join ( commit) ;
236+
237+ // Check if all binaries are already cached
238+ let mut cached_paths = Vec :: new ( ) ;
239+ let mut all_cached = true ;
240+ for ( binary_name, _) in binaries {
241+ let cached_path = cache_dir. join ( binary_name) ;
242+ if !cached_path. exists ( ) {
243+ all_cached = false ;
244+ break ;
245+ }
246+ cached_paths. push ( cached_path) ;
247+ }
248+
249+ if all_cached {
250+ info ! ( self . log, "Found cached binaries for {project} at {commit}" ; "cache_dir" => %cache_dir) ;
251+ return Ok ( cached_paths) ;
252+ }
253+
254+ // Need to build - create temp directory
255+ info ! ( self . log, "Building {project} from source at commit {commit}" ) ;
256+ let temp_dir = tempfile:: tempdir ( ) ?;
257+ let temp_path = Utf8PathBuf :: try_from ( temp_dir. path ( ) . to_path_buf ( ) ) ?;
258+
259+ // Clone and checkout the specific commit
260+ let repo_url = format ! ( "https://github.com/oxidecomputer/{}" , project) ;
261+ info ! ( self . log, "Cloning {repo_url}" ) ;
262+ let mut clone_cmd = Command :: new ( "git" ) ;
263+ clone_cmd
264+ . arg ( "clone" )
265+ . arg ( "--filter=blob:none" )
266+ . arg ( & repo_url)
267+ . arg ( & temp_path) ;
268+
269+ let clone_output = clone_cmd. output ( ) . await ?;
270+ if !clone_output. status . success ( ) {
271+ let stderr = String :: from_utf8_lossy ( & clone_output. stderr ) ;
272+ bail ! ( "Failed to clone {repo_url}: {stderr}" ) ;
273+ }
274+
275+ // Checkout the specific commit
276+ info ! ( self . log, "Checking out commit {commit}" ) ;
277+ let mut checkout_cmd = Command :: new ( "git" ) ;
278+ checkout_cmd. arg ( "checkout" ) . arg ( commit) . current_dir ( & temp_path) ;
279+
280+ let checkout_output = checkout_cmd. output ( ) . await ?;
281+ if !checkout_output. status . success ( ) {
282+ let stderr = String :: from_utf8_lossy ( & checkout_output. stderr ) ;
283+ bail ! ( "Failed to checkout {commit}: {stderr}" ) ;
284+ }
285+
286+ // Build each binary
287+ tokio:: fs:: create_dir_all ( & cache_dir) . await ?;
288+ let mut result_paths = Vec :: new ( ) ;
289+
290+ for ( binary_name, cargo_args) in binaries {
291+ info ! ( self . log, "Building {binary_name}" ; "args" => ?cargo_args) ;
292+
293+ let mut build_cmd = Command :: new ( "cargo" ) ;
294+ build_cmd
295+ . arg ( "build" )
296+ . arg ( "--release" )
297+ . arg ( "--bin" )
298+ . arg ( binary_name)
299+ . args ( * cargo_args)
300+ . current_dir ( & temp_path) ;
301+
302+ let build_output = build_cmd. output ( ) . await ?;
303+ if !build_output. status . success ( ) {
304+ let stderr = String :: from_utf8_lossy ( & build_output. stderr ) ;
305+ bail ! ( "Failed to build {binary_name}: {stderr}" ) ;
306+ }
307+
308+ // Always build in release mode
309+ let source_path =
310+ temp_path. join ( "target" ) . join ( "release" ) . join ( binary_name) ;
311+
312+ if !source_path. exists ( ) {
313+ bail ! ( "Expected binary not found at {source_path}" ) ;
314+ }
315+
316+ // Copy to cache
317+ let cached_path = cache_dir. join ( binary_name) ;
318+ tokio:: fs:: copy ( & source_path, & cached_path) . await ?;
319+ set_permissions ( & cached_path, 0o755 ) . await ?;
320+
321+ result_paths. push ( cached_path) ;
322+ }
323+
324+ info ! ( self . log, "Successfully built and cached {project} binaries" ) ;
325+ Ok ( result_paths)
326+ }
219327}
220328
221329/// Parses a file of the format:
@@ -738,13 +846,24 @@ impl Downloader<'_> {
738846 }
739847 Os :: Illumos => { }
740848 Os :: Mac => {
741- warn ! ( self . log, "WARNING: Dendrite not available for Mac" ) ;
742- warn ! ( self . log, "Network APIs will be unavailable" ) ;
743-
744- let path = bin_dir. join ( "dpd" ) ;
745- tokio:: fs:: write ( & path, "echo 'unsupported os' && exit 1" )
746- . await ?;
747- set_permissions ( & path, 0o755 ) . await ?;
849+ info ! ( self . log, "Building dendrite from source for macOS" ) ;
850+
851+ let binaries = [
852+ ( "dpd" , & [ "--features=tofino_stub" ] [ ..] ) ,
853+ ( "swadm" , & [ ] [ ..] ) ,
854+ ] ;
855+
856+ let built_binaries =
857+ self . build_from_git ( "dendrite" , & commit, & binaries) . await ?;
858+
859+ // Copy built binaries to bin_dir
860+ for ( binary_path, ( binary_name, _) ) in
861+ built_binaries. iter ( ) . zip ( binaries. iter ( ) )
862+ {
863+ let dest = bin_dir. join ( binary_name) ;
864+ tokio:: fs:: copy ( binary_path, & dest) . await ?;
865+ set_permissions ( & dest, 0o755 ) . await ?;
866+ }
748867 }
749868 }
750869
@@ -807,7 +926,21 @@ impl Downloader<'_> {
807926 set_permissions ( & path, 0o755 ) . await ?;
808927 tokio:: fs:: copy ( path, binary_dir. join ( filename) ) . await ?;
809928 }
810- _ => ( ) ,
929+ Os :: Mac => {
930+ info ! ( self . log, "Building maghemite from source for macOS" ) ;
931+
932+ let binaries = [ ( "mgd" , & [ "--no-default-features" ] [ ..] ) ] ;
933+
934+ let built_binaries = self
935+ . build_from_git ( "maghemite" , & commit, & binaries)
936+ . await ?;
937+
938+ // Copy built binary to binary_dir
939+ let dest = binary_dir. join ( "mgd" ) ;
940+ tokio:: fs:: copy ( & built_binaries[ 0 ] , & dest) . await ?;
941+ set_permissions ( & dest, 0o755 ) . await ?;
942+ }
943+ Os :: Illumos => ( ) ,
811944 }
812945
813946 Ok ( ( ) )
0 commit comments