Skip to main content

hydro_lang/deploy/
deploy_graph.rs

1//! Deployment backend for Hydro that uses [`hydro_deploy`] to provision and launch services.
2
3use std::cell::RefCell;
4use std::collections::HashMap;
5use std::future::Future;
6use std::io::Error;
7use std::pin::Pin;
8use std::rc::Rc;
9use std::sync::Arc;
10
11use bytes::{Bytes, BytesMut};
12use dfir_lang::graph::{AsCodeOptions, DfirGraph};
13use futures::{Sink, SinkExt, Stream, StreamExt};
14use hydro_deploy::custom_service::CustomClientPort;
15use hydro_deploy::rust_crate::RustCrateService;
16use hydro_deploy::rust_crate::ports::{DemuxSink, RustCrateSink, RustCrateSource, TaggedSource};
17use hydro_deploy::rust_crate::tracing_options::TracingOptions;
18use hydro_deploy::{CustomService, Deployment, Host, RustCrate};
19use hydro_deploy_integration::{ConnectedSink, ConnectedSource};
20use nameof::name_of;
21use proc_macro2::Span;
22use serde::Serialize;
23use serde::de::DeserializeOwned;
24use slotmap::SparseSecondaryMap;
25use stageleft::{QuotedWithContext, RuntimeData};
26use syn::parse_quote;
27
28use super::deploy_runtime::*;
29use crate::compile::builder::ExternalPortId;
30use crate::compile::deploy_provider::{
31    ClusterSpec, Deploy, ExternalSpec, IntoProcessSpec, Node, ProcessSpec, RegisterPort,
32};
33use crate::compile::trybuild::generate::{
34    HYDRO_RUNTIME_FEATURES, LinkingMode, TrybuildConfig, create_graph_trybuild,
35};
36use crate::location::dynamic::LocationId;
37use crate::location::member_id::TaglessMemberId;
38use crate::location::{LocationKey, NetworkHint};
39use crate::staging_util::get_this_crate;
40
41/// Deployment backend that uses [`hydro_deploy`] for provisioning and launching.
42///
43/// Automatically used when you call [`crate::compile::builder::FlowBuilder::deploy`] and pass in
44/// an `&mut` reference to [`hydro_deploy::Deployment`] as the deployment context.
45pub enum HydroDeploy {}
46
47impl<'a> Deploy<'a> for HydroDeploy {
48    /// Map from Cluster location ID to member IDs.
49    type Meta = SparseSecondaryMap<LocationKey, Vec<TaglessMemberId>>;
50    type InstantiateEnv = Deployment;
51
52    type Process = DeployNode;
53    type Cluster = DeployCluster;
54    type External = DeployExternal;
55
56    fn o2o_sink_source(
57        _env: &mut Self::InstantiateEnv,
58        _p1: &Self::Process,
59        p1_port: &<Self::Process as Node>::Port,
60        _p2: &Self::Process,
61        p2_port: &<Self::Process as Node>::Port,
62        _name: Option<&str>,
63        networking_info: &crate::networking::NetworkingInfo,
64        _external_types: Option<(&syn::Type, &syn::Type)>,
65    ) -> (syn::Expr, syn::Expr) {
66        match networking_info {
67            crate::networking::NetworkingInfo::Tcp {
68                fault: crate::networking::TcpFault::FailStop,
69            } => {}
70            _ => panic!("Unsupported networking info: {:?}", networking_info),
71        }
72        let p1_port = p1_port.as_str();
73        let p2_port = p2_port.as_str();
74        deploy_o2o(
75            RuntimeData::new("__hydro_lang_trybuild_cli"),
76            p1_port,
77            p2_port,
78        )
79    }
80
81    fn o2o_connect(
82        p1: &Self::Process,
83        p1_port: &<Self::Process as Node>::Port,
84        p2: &Self::Process,
85        p2_port: &<Self::Process as Node>::Port,
86    ) -> Box<dyn FnOnce()> {
87        let p1 = p1.clone();
88        let p1_port = p1_port.clone();
89        let p2 = p2.clone();
90        let p2_port = p2_port.clone();
91
92        Box::new(move || {
93            let self_underlying_borrow = p1.underlying.borrow();
94            let self_underlying = self_underlying_borrow.as_ref().unwrap();
95            let source_port = self_underlying.get_port(p1_port.clone());
96
97            let other_underlying_borrow = p2.underlying.borrow();
98            let other_underlying = other_underlying_borrow.as_ref().unwrap();
99            let recipient_port = other_underlying.get_port(p2_port.clone());
100
101            source_port.send_to(&recipient_port)
102        })
103    }
104
105    fn o2m_sink_source(
106        _env: &mut Self::InstantiateEnv,
107        _p1: &Self::Process,
108        p1_port: &<Self::Process as Node>::Port,
109        _c2: &Self::Cluster,
110        c2_port: &<Self::Cluster as Node>::Port,
111        _name: Option<&str>,
112        networking_info: &crate::networking::NetworkingInfo,
113        _external_types: Option<(&syn::Type, &syn::Type)>,
114    ) -> (syn::Expr, syn::Expr) {
115        match networking_info {
116            crate::networking::NetworkingInfo::Tcp {
117                fault: crate::networking::TcpFault::FailStop,
118            } => {}
119            _ => panic!("Unsupported networking info: {:?}", networking_info),
120        }
121        let p1_port = p1_port.as_str();
122        let c2_port = c2_port.as_str();
123        deploy_o2m(
124            RuntimeData::new("__hydro_lang_trybuild_cli"),
125            p1_port,
126            c2_port,
127        )
128    }
129
130    fn o2m_connect(
131        p1: &Self::Process,
132        p1_port: &<Self::Process as Node>::Port,
133        c2: &Self::Cluster,
134        c2_port: &<Self::Cluster as Node>::Port,
135    ) -> Box<dyn FnOnce()> {
136        let p1 = p1.clone();
137        let p1_port = p1_port.clone();
138        let c2 = c2.clone();
139        let c2_port = c2_port.clone();
140
141        Box::new(move || {
142            let self_underlying_borrow = p1.underlying.borrow();
143            let self_underlying = self_underlying_borrow.as_ref().unwrap();
144            let source_port = self_underlying.get_port(p1_port.clone());
145
146            let recipient_port = DemuxSink {
147                demux: c2
148                    .members
149                    .borrow()
150                    .iter()
151                    .enumerate()
152                    .map(|(id, c)| {
153                        (
154                            id as u32,
155                            Arc::new(c.underlying.get_port(c2_port.clone()))
156                                as Arc<dyn RustCrateSink + 'static>,
157                        )
158                    })
159                    .collect(),
160            };
161
162            source_port.send_to(&recipient_port)
163        })
164    }
165
166    fn m2o_sink_source(
167        _env: &mut Self::InstantiateEnv,
168        _c1: &Self::Cluster,
169        c1_port: &<Self::Cluster as Node>::Port,
170        _p2: &Self::Process,
171        p2_port: &<Self::Process as Node>::Port,
172        _name: Option<&str>,
173        networking_info: &crate::networking::NetworkingInfo,
174        _external_types: Option<(&syn::Type, &syn::Type)>,
175    ) -> (syn::Expr, syn::Expr) {
176        match networking_info {
177            crate::networking::NetworkingInfo::Tcp {
178                fault: crate::networking::TcpFault::FailStop,
179            } => {}
180            _ => panic!("Unsupported networking info: {:?}", networking_info),
181        }
182        let c1_port = c1_port.as_str();
183        let p2_port = p2_port.as_str();
184        deploy_m2o(
185            RuntimeData::new("__hydro_lang_trybuild_cli"),
186            c1_port,
187            p2_port,
188        )
189    }
190
191    fn m2o_connect(
192        c1: &Self::Cluster,
193        c1_port: &<Self::Cluster as Node>::Port,
194        p2: &Self::Process,
195        p2_port: &<Self::Process as Node>::Port,
196    ) -> Box<dyn FnOnce()> {
197        let c1 = c1.clone();
198        let c1_port = c1_port.clone();
199        let p2 = p2.clone();
200        let p2_port = p2_port.clone();
201
202        Box::new(move || {
203            let other_underlying_borrow = p2.underlying.borrow();
204            let other_underlying = other_underlying_borrow.as_ref().unwrap();
205            let recipient_port = other_underlying.get_port(p2_port.clone()).merge();
206
207            for (i, node) in c1.members.borrow().iter().enumerate() {
208                let source_port = node.underlying.get_port(c1_port.clone());
209
210                TaggedSource {
211                    source: Arc::new(source_port),
212                    tag: i as u32,
213                }
214                .send_to(&recipient_port);
215            }
216        })
217    }
218
219    fn m2m_sink_source(
220        _env: &mut Self::InstantiateEnv,
221        _c1: &Self::Cluster,
222        c1_port: &<Self::Cluster as Node>::Port,
223        _c2: &Self::Cluster,
224        c2_port: &<Self::Cluster as Node>::Port,
225        _name: Option<&str>,
226        networking_info: &crate::networking::NetworkingInfo,
227        _external_types: Option<(&syn::Type, &syn::Type)>,
228    ) -> (syn::Expr, syn::Expr) {
229        match networking_info {
230            crate::networking::NetworkingInfo::Tcp {
231                fault: crate::networking::TcpFault::FailStop,
232            } => {}
233            _ => panic!("Unsupported networking info: {:?}", networking_info),
234        }
235        let c1_port = c1_port.as_str();
236        let c2_port = c2_port.as_str();
237        deploy_m2m(
238            RuntimeData::new("__hydro_lang_trybuild_cli"),
239            c1_port,
240            c2_port,
241        )
242    }
243
244    fn m2m_connect(
245        c1: &Self::Cluster,
246        c1_port: &<Self::Cluster as Node>::Port,
247        c2: &Self::Cluster,
248        c2_port: &<Self::Cluster as Node>::Port,
249    ) -> Box<dyn FnOnce()> {
250        let c1 = c1.clone();
251        let c1_port = c1_port.clone();
252        let c2 = c2.clone();
253        let c2_port = c2_port.clone();
254
255        Box::new(move || {
256            for (i, sender) in c1.members.borrow().iter().enumerate() {
257                let source_port = sender.underlying.get_port(c1_port.clone());
258
259                let recipient_port = DemuxSink {
260                    demux: c2
261                        .members
262                        .borrow()
263                        .iter()
264                        .enumerate()
265                        .map(|(id, c)| {
266                            (
267                                id as u32,
268                                Arc::new(c.underlying.get_port(c2_port.clone()).merge())
269                                    as Arc<dyn RustCrateSink + 'static>,
270                            )
271                        })
272                        .collect(),
273                };
274
275                TaggedSource {
276                    source: Arc::new(source_port),
277                    tag: i as u32,
278                }
279                .send_to(&recipient_port);
280            }
281        })
282    }
283
284    fn e2o_many_source(
285        extra_stmts: &mut Vec<syn::Stmt>,
286        _p2: &Self::Process,
287        p2_port: &<Self::Process as Node>::Port,
288        codec_type: &syn::Type,
289        shared_handle: String,
290    ) -> syn::Expr {
291        let connect_ident = syn::Ident::new(
292            &format!("__hydro_deploy_many_{}_connect", shared_handle),
293            Span::call_site(),
294        );
295        let source_ident = syn::Ident::new(
296            &format!("__hydro_deploy_many_{}_source", shared_handle),
297            Span::call_site(),
298        );
299        let sink_ident = syn::Ident::new(
300            &format!("__hydro_deploy_many_{}_sink", shared_handle),
301            Span::call_site(),
302        );
303        let membership_ident = syn::Ident::new(
304            &format!("__hydro_deploy_many_{}_membership", shared_handle),
305            Span::call_site(),
306        );
307
308        let root = get_this_crate();
309
310        extra_stmts.push(syn::parse_quote! {
311            let #connect_ident = __hydro_lang_trybuild_cli
312                .port(#p2_port)
313                .connect::<#root::runtime_support::hydro_deploy_integration::multi_connection::ConnectedMultiConnection<_, _, #codec_type>>();
314        });
315
316        extra_stmts.push(syn::parse_quote! {
317            let #source_ident = #connect_ident.source;
318        });
319
320        extra_stmts.push(syn::parse_quote! {
321            let #sink_ident = #connect_ident.sink;
322        });
323
324        extra_stmts.push(syn::parse_quote! {
325            let #membership_ident = #connect_ident.membership;
326        });
327
328        parse_quote!(#source_ident)
329    }
330
331    fn e2o_many_sink(shared_handle: String) -> syn::Expr {
332        let sink_ident = syn::Ident::new(
333            &format!("__hydro_deploy_many_{}_sink", shared_handle),
334            Span::call_site(),
335        );
336        parse_quote!(#sink_ident)
337    }
338
339    fn e2o_source(
340        extra_stmts: &mut Vec<syn::Stmt>,
341        _p1: &Self::External,
342        _p1_port: &<Self::External as Node>::Port,
343        _p2: &Self::Process,
344        p2_port: &<Self::Process as Node>::Port,
345        codec_type: &syn::Type,
346        shared_handle: String,
347    ) -> syn::Expr {
348        let connect_ident = syn::Ident::new(
349            &format!("__hydro_deploy_{}_connect", shared_handle),
350            Span::call_site(),
351        );
352        let source_ident = syn::Ident::new(
353            &format!("__hydro_deploy_{}_source", shared_handle),
354            Span::call_site(),
355        );
356        let sink_ident = syn::Ident::new(
357            &format!("__hydro_deploy_{}_sink", shared_handle),
358            Span::call_site(),
359        );
360
361        let root = get_this_crate();
362
363        extra_stmts.push(syn::parse_quote! {
364            let #connect_ident = __hydro_lang_trybuild_cli
365                .port(#p2_port)
366                .connect::<#root::runtime_support::hydro_deploy_integration::single_connection::ConnectedSingleConnection<_, _, #codec_type>>();
367        });
368
369        extra_stmts.push(syn::parse_quote! {
370            let #source_ident = #connect_ident.source;
371        });
372
373        extra_stmts.push(syn::parse_quote! {
374            let #sink_ident = #connect_ident.sink;
375        });
376
377        parse_quote!(#source_ident)
378    }
379
380    fn e2o_connect(
381        p1: &Self::External,
382        p1_port: &<Self::External as Node>::Port,
383        p2: &Self::Process,
384        p2_port: &<Self::Process as Node>::Port,
385        _many: bool,
386        server_hint: NetworkHint,
387    ) -> Box<dyn FnOnce()> {
388        let p1 = p1.clone();
389        let p1_port = p1_port.clone();
390        let p2 = p2.clone();
391        let p2_port = p2_port.clone();
392
393        Box::new(move || {
394            let self_underlying_borrow = p1.underlying.borrow();
395            let self_underlying = self_underlying_borrow.as_ref().unwrap();
396            let source_port = self_underlying.declare_many_client();
397
398            let other_underlying_borrow = p2.underlying.borrow();
399            let other_underlying = other_underlying_borrow.as_ref().unwrap();
400            let recipient_port = other_underlying.get_port_with_hint(
401                p2_port.clone(),
402                match server_hint {
403                    NetworkHint::Auto => hydro_deploy::PortNetworkHint::Auto,
404                    NetworkHint::TcpPort(p) => hydro_deploy::PortNetworkHint::TcpPort(p),
405                },
406            );
407
408            source_port.send_to(&recipient_port);
409
410            p1.client_ports
411                .borrow_mut()
412                .insert(p1_port.clone(), source_port);
413        })
414    }
415
416    fn o2e_sink(
417        _p1: &Self::Process,
418        _p1_port: &<Self::Process as Node>::Port,
419        _p2: &Self::External,
420        _p2_port: &<Self::External as Node>::Port,
421        shared_handle: String,
422    ) -> syn::Expr {
423        let sink_ident = syn::Ident::new(
424            &format!("__hydro_deploy_{}_sink", shared_handle),
425            Span::call_site(),
426        );
427        parse_quote!(#sink_ident)
428    }
429
430    fn cluster_ids(of_cluster: LocationKey) -> syn::Expr {
431        cluster_members(RuntimeData::new("__hydro_lang_trybuild_cli"), of_cluster).splice_untyped()
432    }
433
434    fn cluster_self_id() -> syn::Expr {
435        cluster_self_id(RuntimeData::new("__hydro_lang_trybuild_cli")).splice_untyped()
436    }
437
438    fn cluster_membership_stream(
439        _env: &mut Self::InstantiateEnv,
440        _at_location: &LocationId,
441        location_id: &LocationId,
442    ) -> syn::Expr {
443        cluster_membership_stream(location_id).splice_untyped()
444    }
445}
446
447#[expect(missing_docs, reason = "TODO")]
448pub trait DeployCrateWrapper {
449    fn underlying(&self) -> Arc<RustCrateService>;
450
451    fn stdout(&self) -> tokio::sync::mpsc::UnboundedReceiver<String> {
452        self.underlying().stdout()
453    }
454
455    fn stderr(&self) -> tokio::sync::mpsc::UnboundedReceiver<String> {
456        self.underlying().stderr()
457    }
458
459    fn stdout_filter(
460        &self,
461        prefix: impl Into<String>,
462    ) -> tokio::sync::mpsc::UnboundedReceiver<String> {
463        self.underlying().stdout_filter(prefix.into())
464    }
465
466    fn stderr_filter(
467        &self,
468        prefix: impl Into<String>,
469    ) -> tokio::sync::mpsc::UnboundedReceiver<String> {
470        self.underlying().stderr_filter(prefix.into())
471    }
472}
473
474#[expect(missing_docs, reason = "TODO")]
475#[derive(Clone)]
476pub struct TrybuildHost {
477    host: Arc<dyn Host>,
478    display_name: Option<String>,
479    rustflags: Option<String>,
480    profile: Option<String>,
481    additional_hydro_features: Vec<String>,
482    features: Vec<String>,
483    tracing: Option<TracingOptions>,
484    build_envs: Vec<(String, String)>,
485    env: HashMap<String, String>,
486    pin_to_core: Option<usize>,
487    name_hint: Option<String>,
488    cluster_idx: Option<usize>,
489}
490
491impl From<Arc<dyn Host>> for TrybuildHost {
492    fn from(host: Arc<dyn Host>) -> Self {
493        Self {
494            host,
495            display_name: None,
496            rustflags: None,
497            profile: None,
498            additional_hydro_features: vec![],
499            features: vec![],
500            tracing: None,
501            build_envs: vec![],
502            env: HashMap::new(),
503            pin_to_core: None,
504            name_hint: None,
505            cluster_idx: None,
506        }
507    }
508}
509
510impl<H: Host + 'static> From<Arc<H>> for TrybuildHost {
511    fn from(host: Arc<H>) -> Self {
512        Self {
513            host,
514            display_name: None,
515            rustflags: None,
516            profile: None,
517            additional_hydro_features: vec![],
518            features: vec![],
519            tracing: None,
520            build_envs: vec![],
521            env: HashMap::new(),
522            pin_to_core: None,
523            name_hint: None,
524            cluster_idx: None,
525        }
526    }
527}
528
529#[expect(missing_docs, reason = "TODO")]
530impl TrybuildHost {
531    pub fn new(host: Arc<dyn Host>) -> Self {
532        Self {
533            host,
534            display_name: None,
535            rustflags: None,
536            profile: None,
537            additional_hydro_features: vec![],
538            features: vec![],
539            tracing: None,
540            build_envs: vec![],
541            env: HashMap::new(),
542            pin_to_core: None,
543            name_hint: None,
544            cluster_idx: None,
545        }
546    }
547
548    pub fn display_name(self, display_name: impl Into<String>) -> Self {
549        assert!(
550            self.display_name.is_none(),
551            "{} already set",
552            name_of!(display_name in Self)
553        );
554
555        Self {
556            display_name: Some(display_name.into()),
557            ..self
558        }
559    }
560
561    pub fn rustflags(self, rustflags: impl Into<String>) -> Self {
562        assert!(
563            self.rustflags.is_none(),
564            "{} already set",
565            name_of!(rustflags in Self)
566        );
567
568        Self {
569            rustflags: Some(rustflags.into()),
570            ..self
571        }
572    }
573
574    pub fn profile(self, profile: impl Into<String>) -> Self {
575        assert!(
576            self.profile.is_none(),
577            "{} already set",
578            name_of!(profile in Self)
579        );
580
581        Self {
582            profile: Some(profile.into()),
583            ..self
584        }
585    }
586
587    pub fn additional_hydro_features(
588        mut self,
589        additional_hydro_features: impl IntoIterator<Item = impl Into<String>>,
590    ) -> Self {
591        self.additional_hydro_features
592            .extend(additional_hydro_features.into_iter().map(Into::into));
593        self
594    }
595
596    pub fn additional_hydro_feature(mut self, feature: impl Into<String>) -> Self {
597        self.additional_hydro_features.push(feature.into());
598        self
599    }
600
601    pub fn features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
602        self.features.extend(features.into_iter().map(Into::into));
603        self
604    }
605
606    pub fn feature(mut self, feature: impl Into<String>) -> Self {
607        self.features.push(feature.into());
608        self
609    }
610
611    #[must_use]
612    pub fn tracing(self, tracing: TracingOptions) -> Self {
613        assert!(
614            self.tracing.is_none(),
615            "{} already set",
616            name_of!(tracing in Self)
617        );
618
619        Self {
620            tracing: Some(tracing),
621            ..self
622        }
623    }
624
625    pub fn build_env(self, key: impl Into<String>, value: impl Into<String>) -> Self {
626        Self {
627            build_envs: self
628                .build_envs
629                .into_iter()
630                .chain(std::iter::once((key.into(), value.into())))
631                .collect(),
632            ..self
633        }
634    }
635
636    pub fn env(self, key: impl Into<String>, value: impl Into<String>) -> Self {
637        let mut env = self.env;
638        env.insert(key.into(), value.into());
639        Self { env, ..self }
640    }
641
642    #[must_use]
643    pub fn pin_to_core(self, core: usize) -> Self {
644        Self {
645            pin_to_core: Some(core),
646            ..self
647        }
648    }
649}
650
651impl IntoProcessSpec<'_, HydroDeploy> for Arc<dyn Host> {
652    type ProcessSpec = TrybuildHost;
653    fn into_process_spec(self) -> TrybuildHost {
654        TrybuildHost {
655            host: self,
656            display_name: None,
657            rustflags: None,
658            profile: None,
659            additional_hydro_features: vec![],
660            features: vec![],
661            tracing: None,
662            build_envs: vec![],
663            env: HashMap::new(),
664            pin_to_core: None,
665            name_hint: None,
666            cluster_idx: None,
667        }
668    }
669}
670
671impl<H: Host + 'static> IntoProcessSpec<'_, HydroDeploy> for Arc<H> {
672    type ProcessSpec = TrybuildHost;
673    fn into_process_spec(self) -> TrybuildHost {
674        TrybuildHost {
675            host: self,
676            display_name: None,
677            rustflags: None,
678            profile: None,
679            additional_hydro_features: vec![],
680            features: vec![],
681            tracing: None,
682            build_envs: vec![],
683            env: HashMap::new(),
684            pin_to_core: None,
685            name_hint: None,
686            cluster_idx: None,
687        }
688    }
689}
690
691#[expect(missing_docs, reason = "TODO")]
692#[derive(Clone)]
693pub struct DeployExternal {
694    next_port: Rc<RefCell<usize>>,
695    host: Arc<dyn Host>,
696    underlying: Rc<RefCell<Option<Arc<CustomService>>>>,
697    client_ports: Rc<RefCell<HashMap<String, CustomClientPort>>>,
698    allocated_ports: Rc<RefCell<HashMap<ExternalPortId, String>>>,
699}
700
701impl DeployExternal {
702    pub(crate) fn raw_port(&self, external_port_id: ExternalPortId) -> CustomClientPort {
703        self.client_ports
704            .borrow()
705            .get(
706                self.allocated_ports
707                    .borrow()
708                    .get(&external_port_id)
709                    .unwrap(),
710            )
711            .unwrap()
712            .clone()
713    }
714}
715
716impl<'a> RegisterPort<'a, HydroDeploy> for DeployExternal {
717    fn register(&self, external_port_id: ExternalPortId, port: Self::Port) {
718        assert!(
719            self.allocated_ports
720                .borrow_mut()
721                .insert(external_port_id, port.clone())
722                .is_none_or(|old| old == port)
723        );
724    }
725
726    fn as_bytes_bidi(
727        &self,
728        external_port_id: ExternalPortId,
729    ) -> impl Future<
730        Output = (
731            Pin<Box<dyn Stream<Item = Result<BytesMut, Error>>>>,
732            Pin<Box<dyn Sink<Bytes, Error = Error>>>,
733        ),
734    > + 'a {
735        let port = self.raw_port(external_port_id);
736
737        async move {
738            let (source, sink) = port.connect().await.into_source_sink();
739            (
740                Box::pin(source) as Pin<Box<dyn Stream<Item = Result<BytesMut, Error>>>>,
741                Box::pin(sink) as Pin<Box<dyn Sink<Bytes, Error = Error>>>,
742            )
743        }
744    }
745
746    fn as_bincode_bidi<InT, OutT>(
747        &self,
748        external_port_id: ExternalPortId,
749    ) -> impl Future<
750        Output = (
751            Pin<Box<dyn Stream<Item = OutT>>>,
752            Pin<Box<dyn Sink<InT, Error = Error>>>,
753        ),
754    > + 'a
755    where
756        InT: Serialize + 'static,
757        OutT: DeserializeOwned + 'static,
758    {
759        let port = self.raw_port(external_port_id);
760        async move {
761            let (source, sink) = port.connect().await.into_source_sink();
762            (
763                Box::pin(source.map(|item| bincode::deserialize(&item.unwrap()).unwrap()))
764                    as Pin<Box<dyn Stream<Item = OutT>>>,
765                Box::pin(
766                    sink.with(|item| async move { Ok(bincode::serialize(&item).unwrap().into()) }),
767                ) as Pin<Box<dyn Sink<InT, Error = Error>>>,
768            )
769        }
770    }
771
772    fn as_bincode_sink<T: Serialize + 'static>(
773        &self,
774        external_port_id: ExternalPortId,
775    ) -> impl Future<Output = Pin<Box<dyn Sink<T, Error = Error>>>> + 'a {
776        let port = self.raw_port(external_port_id);
777        async move {
778            let sink = port.connect().await.into_sink();
779            Box::pin(sink.with(|item| async move { Ok(bincode::serialize(&item).unwrap().into()) }))
780                as Pin<Box<dyn Sink<T, Error = Error>>>
781        }
782    }
783
784    fn as_bincode_source<T: DeserializeOwned + 'static>(
785        &self,
786        external_port_id: ExternalPortId,
787    ) -> impl Future<Output = Pin<Box<dyn Stream<Item = T>>>> + 'a {
788        let port = self.raw_port(external_port_id);
789        async move {
790            let source = port.connect().await.into_source();
791            Box::pin(source.map(|item| bincode::deserialize(&item.unwrap()).unwrap()))
792                as Pin<Box<dyn Stream<Item = T>>>
793        }
794    }
795}
796
797impl Node for DeployExternal {
798    type Port = String;
799    /// Map from Cluster location ID to member IDs.
800    type Meta = SparseSecondaryMap<LocationKey, Vec<TaglessMemberId>>;
801    type InstantiateEnv = Deployment;
802
803    fn next_port(&self) -> Self::Port {
804        let next_port = *self.next_port.borrow();
805        *self.next_port.borrow_mut() += 1;
806
807        format!("port_{}", next_port)
808    }
809
810    fn instantiate(
811        &self,
812        env: &mut Self::InstantiateEnv,
813        _meta: &mut Self::Meta,
814        _graph: DfirGraph,
815        extra_stmts: &[syn::Stmt],
816        sidecars: &[syn::Expr],
817        _as_code_options: &AsCodeOptions,
818    ) {
819        assert!(extra_stmts.is_empty());
820        assert!(sidecars.is_empty());
821        let service = env.CustomService(self.host.clone(), vec![]);
822        *self.underlying.borrow_mut() = Some(service);
823    }
824
825    fn update_meta(&self, _meta: &Self::Meta) {}
826}
827
828impl ExternalSpec<'_, HydroDeploy> for Arc<dyn Host> {
829    fn build(self, _key: LocationKey, _name_hint: &str) -> DeployExternal {
830        DeployExternal {
831            next_port: Rc::new(RefCell::new(0)),
832            host: self,
833            underlying: Rc::new(RefCell::new(None)),
834            allocated_ports: Rc::new(RefCell::new(HashMap::new())),
835            client_ports: Rc::new(RefCell::new(HashMap::new())),
836        }
837    }
838}
839
840impl<H: Host + 'static> ExternalSpec<'_, HydroDeploy> for Arc<H> {
841    fn build(self, _key: LocationKey, _name_hint: &str) -> DeployExternal {
842        DeployExternal {
843            next_port: Rc::new(RefCell::new(0)),
844            host: self,
845            underlying: Rc::new(RefCell::new(None)),
846            allocated_ports: Rc::new(RefCell::new(HashMap::new())),
847            client_ports: Rc::new(RefCell::new(HashMap::new())),
848        }
849    }
850}
851
852pub(crate) enum CrateOrTrybuild {
853    Crate(RustCrate, Arc<dyn Host>),
854    Trybuild(TrybuildHost),
855}
856
857#[expect(missing_docs, reason = "TODO")]
858#[derive(Clone)]
859pub struct DeployNode {
860    next_port: Rc<RefCell<usize>>,
861    service_spec: Rc<RefCell<Option<CrateOrTrybuild>>>,
862    underlying: Rc<RefCell<Option<Arc<RustCrateService>>>>,
863}
864
865impl DeployCrateWrapper for DeployNode {
866    fn underlying(&self) -> Arc<RustCrateService> {
867        Arc::clone(self.underlying.borrow().as_ref().unwrap())
868    }
869}
870
871impl Node for DeployNode {
872    type Port = String;
873    /// Map from Cluster location ID to member IDs.
874    type Meta = SparseSecondaryMap<LocationKey, Vec<TaglessMemberId>>;
875    type InstantiateEnv = Deployment;
876
877    fn next_port(&self) -> String {
878        let next_port = *self.next_port.borrow();
879        *self.next_port.borrow_mut() += 1;
880
881        format!("port_{}", next_port)
882    }
883
884    fn update_meta(&self, meta: &Self::Meta) {
885        let underlying_node = self.underlying.borrow();
886        underlying_node.as_ref().unwrap().update_meta(HydroMeta {
887            clusters: meta.clone(),
888            cluster_id: None,
889        });
890    }
891
892    fn instantiate(
893        &self,
894        env: &mut Self::InstantiateEnv,
895        _meta: &mut Self::Meta,
896        graph: DfirGraph,
897        extra_stmts: &[syn::Stmt],
898        sidecars: &[syn::Expr],
899        as_code_options: &AsCodeOptions,
900    ) {
901        let (service, host) = match self.service_spec.borrow_mut().take().unwrap() {
902            CrateOrTrybuild::Crate(c, host) => (c, host),
903            CrateOrTrybuild::Trybuild(trybuild) => {
904                // Determine linking mode based on host target type
905                let linking_mode = if !cfg!(target_os = "windows")
906                    && trybuild.host.target_type() == hydro_deploy::HostTargetType::Local
907                    && trybuild.rustflags.is_none()
908                {
909                    // When compiling for local, prefer dynamic linking to reduce binary size
910                    // Windows is currently not supported due to https://github.com/bevyengine/bevy/pull/2016
911                    LinkingMode::Dynamic
912                } else {
913                    LinkingMode::Static
914                };
915                let (bin_name, config) = create_graph_trybuild(
916                    graph,
917                    extra_stmts,
918                    sidecars,
919                    as_code_options,
920                    trybuild.name_hint.as_deref(),
921                    crate::compile::trybuild::generate::DeployMode::HydroDeploy,
922                    linking_mode,
923                );
924                let host = trybuild.host.clone();
925                (create_trybuild_service(trybuild, &config, &bin_name), host)
926            }
927        };
928
929        *self.underlying.borrow_mut() = Some(env.add_service(service, host));
930    }
931}
932
933#[expect(missing_docs, reason = "TODO")]
934#[derive(Clone)]
935pub struct DeployClusterNode {
936    underlying: Arc<RustCrateService>,
937}
938
939impl DeployCrateWrapper for DeployClusterNode {
940    fn underlying(&self) -> Arc<RustCrateService> {
941        self.underlying.clone()
942    }
943}
944#[expect(missing_docs, reason = "TODO")]
945#[derive(Clone)]
946pub struct DeployCluster {
947    key: LocationKey,
948    next_port: Rc<RefCell<usize>>,
949    cluster_spec: Rc<RefCell<Option<Vec<CrateOrTrybuild>>>>,
950    members: Rc<RefCell<Vec<DeployClusterNode>>>,
951    name_hint: Option<String>,
952}
953
954impl DeployCluster {
955    #[expect(missing_docs, reason = "TODO")]
956    pub fn members(&self) -> Vec<DeployClusterNode> {
957        self.members.borrow().clone()
958    }
959}
960
961impl Node for DeployCluster {
962    type Port = String;
963    /// Map from Cluster location ID to member IDs.
964    type Meta = SparseSecondaryMap<LocationKey, Vec<TaglessMemberId>>;
965    type InstantiateEnv = Deployment;
966
967    fn next_port(&self) -> String {
968        let next_port = *self.next_port.borrow();
969        *self.next_port.borrow_mut() += 1;
970
971        format!("port_{}", next_port)
972    }
973
974    fn instantiate(
975        &self,
976        env: &mut Self::InstantiateEnv,
977        meta: &mut Self::Meta,
978        graph: DfirGraph,
979        extra_stmts: &[syn::Stmt],
980        sidecars: &[syn::Expr],
981        as_code_options: &AsCodeOptions,
982    ) {
983        let has_trybuild = self
984            .cluster_spec
985            .borrow()
986            .as_ref()
987            .unwrap()
988            .iter()
989            .any(|spec| matches!(spec, CrateOrTrybuild::Trybuild { .. }));
990
991        // For clusters, use static linking if ANY host is non-local (conservative approach)
992        let linking_mode = if !cfg!(target_os = "windows")
993            && self
994                .cluster_spec
995                .borrow()
996                .as_ref()
997                .unwrap()
998                .iter()
999                .all(|spec| match spec {
1000                    CrateOrTrybuild::Crate(_, _) => true, // crates handle their own linking
1001                    CrateOrTrybuild::Trybuild(t) => {
1002                        t.host.target_type() == hydro_deploy::HostTargetType::Local
1003                            && t.rustflags.is_none()
1004                    }
1005                }) {
1006            // See comment above for Windows exception
1007            LinkingMode::Dynamic
1008        } else {
1009            LinkingMode::Static
1010        };
1011
1012        let maybe_trybuild = if has_trybuild {
1013            Some(create_graph_trybuild(
1014                graph,
1015                extra_stmts,
1016                sidecars,
1017                as_code_options,
1018                self.name_hint.as_deref(),
1019                crate::compile::trybuild::generate::DeployMode::HydroDeploy,
1020                linking_mode,
1021            ))
1022        } else {
1023            None
1024        };
1025
1026        let cluster_nodes = self
1027            .cluster_spec
1028            .borrow_mut()
1029            .take()
1030            .unwrap()
1031            .into_iter()
1032            .map(|spec| {
1033                let (service, host) = match spec {
1034                    CrateOrTrybuild::Crate(c, host) => (c, host),
1035                    CrateOrTrybuild::Trybuild(trybuild) => {
1036                        let (bin_name, config) = maybe_trybuild.as_ref().unwrap();
1037                        let host = trybuild.host.clone();
1038                        (create_trybuild_service(trybuild, config, bin_name), host)
1039                    }
1040                };
1041
1042                env.add_service(service, host)
1043            })
1044            .collect::<Vec<_>>();
1045        meta.insert(
1046            self.key,
1047            (0..(cluster_nodes.len() as u32))
1048                .map(TaglessMemberId::from_raw_id)
1049                .collect(),
1050        );
1051        *self.members.borrow_mut() = cluster_nodes
1052            .into_iter()
1053            .map(|n| DeployClusterNode { underlying: n })
1054            .collect();
1055    }
1056
1057    fn update_meta(&self, meta: &Self::Meta) {
1058        for (cluster_id, node) in self.members.borrow().iter().enumerate() {
1059            node.underlying.update_meta(HydroMeta {
1060                clusters: meta.clone(),
1061                cluster_id: Some(TaglessMemberId::from_raw_id(cluster_id as u32)),
1062            });
1063        }
1064    }
1065}
1066
1067#[expect(missing_docs, reason = "TODO")]
1068#[derive(Clone)]
1069pub struct DeployProcessSpec(RustCrate, Arc<dyn Host>);
1070
1071impl DeployProcessSpec {
1072    #[expect(missing_docs, reason = "TODO")]
1073    pub fn new(t: RustCrate, host: Arc<dyn Host>) -> Self {
1074        Self(t, host)
1075    }
1076}
1077
1078impl ProcessSpec<'_, HydroDeploy> for DeployProcessSpec {
1079    fn build(self, _key: LocationKey, _name_hint: &str) -> DeployNode {
1080        DeployNode {
1081            next_port: Rc::new(RefCell::new(0)),
1082            service_spec: Rc::new(RefCell::new(Some(CrateOrTrybuild::Crate(self.0, self.1)))),
1083            underlying: Rc::new(RefCell::new(None)),
1084        }
1085    }
1086}
1087
1088impl ProcessSpec<'_, HydroDeploy> for TrybuildHost {
1089    fn build(mut self, key: LocationKey, name_hint: &str) -> DeployNode {
1090        self.name_hint = Some(format!("{} (process {})", name_hint, key));
1091        DeployNode {
1092            next_port: Rc::new(RefCell::new(0)),
1093            service_spec: Rc::new(RefCell::new(Some(CrateOrTrybuild::Trybuild(self)))),
1094            underlying: Rc::new(RefCell::new(None)),
1095        }
1096    }
1097}
1098
1099#[expect(missing_docs, reason = "TODO")]
1100#[derive(Clone)]
1101pub struct DeployClusterSpec(Vec<(RustCrate, Arc<dyn Host>)>);
1102
1103impl DeployClusterSpec {
1104    #[expect(missing_docs, reason = "TODO")]
1105    pub fn new(crates: Vec<(RustCrate, Arc<dyn Host>)>) -> Self {
1106        Self(crates)
1107    }
1108}
1109
1110impl ClusterSpec<'_, HydroDeploy> for DeployClusterSpec {
1111    fn build(self, key: LocationKey, _name_hint: &str) -> DeployCluster {
1112        DeployCluster {
1113            key,
1114            next_port: Rc::new(RefCell::new(0)),
1115            cluster_spec: Rc::new(RefCell::new(Some(
1116                self.0
1117                    .into_iter()
1118                    .map(|(c, h)| CrateOrTrybuild::Crate(c, h))
1119                    .collect(),
1120            ))),
1121            members: Rc::new(RefCell::new(vec![])),
1122            name_hint: None,
1123        }
1124    }
1125}
1126
1127impl<T: Into<TrybuildHost>, I: IntoIterator<Item = T>> ClusterSpec<'_, HydroDeploy> for I {
1128    fn build(self, key: LocationKey, name_hint: &str) -> DeployCluster {
1129        let name_hint = format!("{} (cluster {})", name_hint, key);
1130        DeployCluster {
1131            key,
1132            next_port: Rc::new(RefCell::new(0)),
1133            cluster_spec: Rc::new(RefCell::new(Some(
1134                self.into_iter()
1135                    .enumerate()
1136                    .map(|(idx, b)| {
1137                        let mut b = b.into();
1138                        b.name_hint = Some(name_hint.clone());
1139                        b.cluster_idx = Some(idx);
1140                        CrateOrTrybuild::Trybuild(b)
1141                    })
1142                    .collect(),
1143            ))),
1144            members: Rc::new(RefCell::new(vec![])),
1145            name_hint: Some(name_hint),
1146        }
1147    }
1148}
1149
1150fn create_trybuild_service(
1151    trybuild: TrybuildHost,
1152    config: &TrybuildConfig,
1153    bin_name: &str,
1154) -> RustCrate {
1155    let dir = &config.project_dir;
1156    // For dynamic linking, use the dylib-examples crate; for static, use the base crate
1157    let crate_dir = match config.linking_mode {
1158        LinkingMode::Dynamic => dir.join("dylib-examples"),
1159        LinkingMode::Static => dir.clone(),
1160    };
1161
1162    let mut ret = RustCrate::new(&crate_dir, dir)
1163        .target_dir(&config.target_dir)
1164        .example(bin_name)
1165        .no_default_features();
1166
1167    if matches!(config.linking_mode, LinkingMode::Dynamic) {
1168        ret = ret.dylib_package(&config.dylib_package_name);
1169    }
1170
1171    if let Some(display_name) = trybuild.display_name {
1172        ret = ret.display_name(display_name);
1173    } else if let Some(name_hint) = trybuild.name_hint {
1174        if let Some(cluster_idx) = trybuild.cluster_idx {
1175            ret = ret.display_name(format!("{} / {}", name_hint, cluster_idx));
1176        } else {
1177            ret = ret.display_name(name_hint);
1178        }
1179    }
1180
1181    if let Some(rustflags) = trybuild.rustflags {
1182        ret = ret.rustflags(rustflags);
1183    }
1184
1185    if let Some(profile) = trybuild.profile {
1186        ret = ret.profile(profile);
1187    }
1188
1189    if let Some(tracing) = trybuild.tracing {
1190        ret = ret.tracing(tracing);
1191    }
1192
1193    if let Some(core) = trybuild.pin_to_core {
1194        ret = ret.pin_to_core(core);
1195    }
1196
1197    ret = ret.features(
1198        vec!["hydro___feature_deploy_integration".to_owned()]
1199            .into_iter()
1200            .chain(
1201                trybuild
1202                    .additional_hydro_features
1203                    .into_iter()
1204                    .map(|runtime_feature| {
1205                        assert!(
1206                            HYDRO_RUNTIME_FEATURES.iter().any(|f| f == &runtime_feature),
1207                            "{runtime_feature} is not a valid Hydro runtime feature"
1208                        );
1209                        format!("hydro___feature_{runtime_feature}")
1210                    }),
1211            )
1212            .chain(trybuild.features),
1213    );
1214
1215    for (key, value) in trybuild.build_envs {
1216        ret = ret.build_env(key, value);
1217    }
1218
1219    for (key, value) in trybuild.env {
1220        ret = ret.env(key, value);
1221    }
1222
1223    ret = ret.build_env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
1224    ret = ret.config("build.incremental = false");
1225
1226    if let Some(features) = config.features.as_deref() {
1227        ret = ret.features(features);
1228    }
1229
1230    ret
1231}