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::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, create_graph_trybuild,
35};
36use crate::location::dynamic::LocationId;
37use crate::location::member_id::TaglessMemberId;
38use crate::location::{LocationKey, MembershipEvent, 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(
431        of_cluster: LocationKey,
432    ) -> impl QuotedWithContext<'a, &'a [TaglessMemberId], ()> + Clone + 'a {
433        cluster_members(RuntimeData::new("__hydro_lang_trybuild_cli"), of_cluster)
434    }
435
436    fn cluster_self_id() -> impl QuotedWithContext<'a, TaglessMemberId, ()> + Clone + 'a {
437        cluster_self_id(RuntimeData::new("__hydro_lang_trybuild_cli"))
438    }
439
440    fn cluster_membership_stream(
441        _env: &mut Self::InstantiateEnv,
442        _at_location: &LocationId,
443        location_id: &LocationId,
444    ) -> impl QuotedWithContext<'a, Box<dyn Stream<Item = (TaglessMemberId, MembershipEvent)> + Unpin>, ()>
445    {
446        cluster_membership_stream(location_id)
447    }
448}
449
450#[expect(missing_docs, reason = "TODO")]
451pub trait DeployCrateWrapper {
452    fn underlying(&self) -> Arc<RustCrateService>;
453
454    fn stdout(&self) -> tokio::sync::mpsc::UnboundedReceiver<String> {
455        self.underlying().stdout()
456    }
457
458    fn stderr(&self) -> tokio::sync::mpsc::UnboundedReceiver<String> {
459        self.underlying().stderr()
460    }
461
462    fn stdout_filter(
463        &self,
464        prefix: impl Into<String>,
465    ) -> tokio::sync::mpsc::UnboundedReceiver<String> {
466        self.underlying().stdout_filter(prefix.into())
467    }
468
469    fn stderr_filter(
470        &self,
471        prefix: impl Into<String>,
472    ) -> tokio::sync::mpsc::UnboundedReceiver<String> {
473        self.underlying().stderr_filter(prefix.into())
474    }
475}
476
477#[expect(missing_docs, reason = "TODO")]
478#[derive(Clone)]
479pub struct TrybuildHost {
480    host: Arc<dyn Host>,
481    display_name: Option<String>,
482    rustflags: Option<String>,
483    profile: Option<String>,
484    additional_hydro_features: Vec<String>,
485    features: Vec<String>,
486    tracing: Option<TracingOptions>,
487    build_envs: Vec<(String, String)>,
488    env: HashMap<String, String>,
489    pin_to_core: Option<usize>,
490    name_hint: Option<String>,
491    cluster_idx: Option<usize>,
492}
493
494impl From<Arc<dyn Host>> for TrybuildHost {
495    fn from(host: Arc<dyn Host>) -> Self {
496        Self {
497            host,
498            display_name: None,
499            rustflags: None,
500            profile: None,
501            additional_hydro_features: vec![],
502            features: vec![],
503            tracing: None,
504            build_envs: vec![],
505            env: HashMap::new(),
506            pin_to_core: None,
507            name_hint: None,
508            cluster_idx: None,
509        }
510    }
511}
512
513impl<H: Host + 'static> From<Arc<H>> for TrybuildHost {
514    fn from(host: Arc<H>) -> Self {
515        Self {
516            host,
517            display_name: None,
518            rustflags: None,
519            profile: None,
520            additional_hydro_features: vec![],
521            features: vec![],
522            tracing: None,
523            build_envs: vec![],
524            env: HashMap::new(),
525            pin_to_core: None,
526            name_hint: None,
527            cluster_idx: None,
528        }
529    }
530}
531
532#[expect(missing_docs, reason = "TODO")]
533impl TrybuildHost {
534    pub fn new(host: Arc<dyn Host>) -> Self {
535        Self {
536            host,
537            display_name: None,
538            rustflags: None,
539            profile: None,
540            additional_hydro_features: vec![],
541            features: vec![],
542            tracing: None,
543            build_envs: vec![],
544            env: HashMap::new(),
545            pin_to_core: None,
546            name_hint: None,
547            cluster_idx: None,
548        }
549    }
550
551    pub fn display_name(self, display_name: impl Into<String>) -> Self {
552        if self.display_name.is_some() {
553            panic!("{} already set", name_of!(display_name in Self));
554        }
555
556        Self {
557            display_name: Some(display_name.into()),
558            ..self
559        }
560    }
561
562    pub fn rustflags(self, rustflags: impl Into<String>) -> Self {
563        if self.rustflags.is_some() {
564            panic!("{} already set", name_of!(rustflags in Self));
565        }
566
567        Self {
568            rustflags: Some(rustflags.into()),
569            ..self
570        }
571    }
572
573    pub fn profile(self, profile: impl Into<String>) -> Self {
574        if self.profile.is_some() {
575            panic!("{} already set", name_of!(profile in Self));
576        }
577
578        Self {
579            profile: Some(profile.into()),
580            ..self
581        }
582    }
583
584    pub fn additional_hydro_features(
585        mut self,
586        additional_hydro_features: impl IntoIterator<Item = impl Into<String>>,
587    ) -> Self {
588        self.additional_hydro_features
589            .extend(additional_hydro_features.into_iter().map(Into::into));
590        self
591    }
592
593    pub fn additional_hydro_feature(mut self, feature: impl Into<String>) -> Self {
594        self.additional_hydro_features.push(feature.into());
595        self
596    }
597
598    pub fn features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
599        self.features.extend(features.into_iter().map(Into::into));
600        self
601    }
602
603    pub fn feature(mut self, feature: impl Into<String>) -> Self {
604        self.features.push(feature.into());
605        self
606    }
607
608    pub fn tracing(self, tracing: TracingOptions) -> Self {
609        if self.tracing.is_some() {
610            panic!("{} already set", name_of!(tracing in Self));
611        }
612
613        Self {
614            tracing: Some(tracing),
615            ..self
616        }
617    }
618
619    pub fn build_env(self, key: impl Into<String>, value: impl Into<String>) -> Self {
620        Self {
621            build_envs: self
622                .build_envs
623                .into_iter()
624                .chain(std::iter::once((key.into(), value.into())))
625                .collect(),
626            ..self
627        }
628    }
629
630    pub fn env(self, key: impl Into<String>, value: impl Into<String>) -> Self {
631        let mut env = self.env;
632        env.insert(key.into(), value.into());
633        Self { env, ..self }
634    }
635
636    pub fn pin_to_core(self, core: usize) -> Self {
637        Self {
638            pin_to_core: Some(core),
639            ..self
640        }
641    }
642}
643
644impl IntoProcessSpec<'_, HydroDeploy> for Arc<dyn Host> {
645    type ProcessSpec = TrybuildHost;
646    fn into_process_spec(self) -> TrybuildHost {
647        TrybuildHost {
648            host: self,
649            display_name: None,
650            rustflags: None,
651            profile: None,
652            additional_hydro_features: vec![],
653            features: vec![],
654            tracing: None,
655            build_envs: vec![],
656            env: HashMap::new(),
657            pin_to_core: None,
658            name_hint: None,
659            cluster_idx: None,
660        }
661    }
662}
663
664impl<H: Host + 'static> IntoProcessSpec<'_, HydroDeploy> for Arc<H> {
665    type ProcessSpec = TrybuildHost;
666    fn into_process_spec(self) -> TrybuildHost {
667        TrybuildHost {
668            host: self,
669            display_name: None,
670            rustflags: None,
671            profile: None,
672            additional_hydro_features: vec![],
673            features: vec![],
674            tracing: None,
675            build_envs: vec![],
676            env: HashMap::new(),
677            pin_to_core: None,
678            name_hint: None,
679            cluster_idx: None,
680        }
681    }
682}
683
684#[expect(missing_docs, reason = "TODO")]
685#[derive(Clone)]
686pub struct DeployExternal {
687    next_port: Rc<RefCell<usize>>,
688    host: Arc<dyn Host>,
689    underlying: Rc<RefCell<Option<Arc<CustomService>>>>,
690    client_ports: Rc<RefCell<HashMap<String, CustomClientPort>>>,
691    allocated_ports: Rc<RefCell<HashMap<ExternalPortId, String>>>,
692}
693
694impl DeployExternal {
695    pub(crate) fn raw_port(&self, external_port_id: ExternalPortId) -> CustomClientPort {
696        self.client_ports
697            .borrow()
698            .get(
699                self.allocated_ports
700                    .borrow()
701                    .get(&external_port_id)
702                    .unwrap(),
703            )
704            .unwrap()
705            .clone()
706    }
707}
708
709impl<'a> RegisterPort<'a, HydroDeploy> for DeployExternal {
710    fn register(&self, external_port_id: ExternalPortId, port: Self::Port) {
711        assert!(
712            self.allocated_ports
713                .borrow_mut()
714                .insert(external_port_id, port.clone())
715                .is_none_or(|old| old == port)
716        );
717    }
718
719    fn as_bytes_bidi(
720        &self,
721        external_port_id: ExternalPortId,
722    ) -> impl Future<
723        Output = (
724            Pin<Box<dyn Stream<Item = Result<BytesMut, Error>>>>,
725            Pin<Box<dyn Sink<Bytes, Error = Error>>>,
726        ),
727    > + 'a {
728        let port = self.raw_port(external_port_id);
729
730        async move {
731            let (source, sink) = port.connect().await.into_source_sink();
732            (
733                Box::pin(source) as Pin<Box<dyn Stream<Item = Result<BytesMut, Error>>>>,
734                Box::pin(sink) as Pin<Box<dyn Sink<Bytes, Error = Error>>>,
735            )
736        }
737    }
738
739    fn as_bincode_bidi<InT, OutT>(
740        &self,
741        external_port_id: ExternalPortId,
742    ) -> impl Future<
743        Output = (
744            Pin<Box<dyn Stream<Item = OutT>>>,
745            Pin<Box<dyn Sink<InT, Error = Error>>>,
746        ),
747    > + 'a
748    where
749        InT: Serialize + 'static,
750        OutT: DeserializeOwned + 'static,
751    {
752        let port = self.raw_port(external_port_id);
753        async move {
754            let (source, sink) = port.connect().await.into_source_sink();
755            (
756                Box::pin(source.map(|item| bincode::deserialize(&item.unwrap()).unwrap()))
757                    as Pin<Box<dyn Stream<Item = OutT>>>,
758                Box::pin(
759                    sink.with(|item| async move { Ok(bincode::serialize(&item).unwrap().into()) }),
760                ) as Pin<Box<dyn Sink<InT, Error = Error>>>,
761            )
762        }
763    }
764
765    fn as_bincode_sink<T: Serialize + 'static>(
766        &self,
767        external_port_id: ExternalPortId,
768    ) -> impl Future<Output = Pin<Box<dyn Sink<T, Error = Error>>>> + 'a {
769        let port = self.raw_port(external_port_id);
770        async move {
771            let sink = port.connect().await.into_sink();
772            Box::pin(sink.with(|item| async move { Ok(bincode::serialize(&item).unwrap().into()) }))
773                as Pin<Box<dyn Sink<T, Error = Error>>>
774        }
775    }
776
777    fn as_bincode_source<T: DeserializeOwned + 'static>(
778        &self,
779        external_port_id: ExternalPortId,
780    ) -> impl Future<Output = Pin<Box<dyn Stream<Item = T>>>> + 'a {
781        let port = self.raw_port(external_port_id);
782        async move {
783            let source = port.connect().await.into_source();
784            Box::pin(source.map(|item| bincode::deserialize(&item.unwrap()).unwrap()))
785                as Pin<Box<dyn Stream<Item = T>>>
786        }
787    }
788}
789
790impl Node for DeployExternal {
791    type Port = String;
792    /// Map from Cluster location ID to member IDs.
793    type Meta = SparseSecondaryMap<LocationKey, Vec<TaglessMemberId>>;
794    type InstantiateEnv = Deployment;
795
796    fn next_port(&self) -> Self::Port {
797        let next_port = *self.next_port.borrow();
798        *self.next_port.borrow_mut() += 1;
799
800        format!("port_{}", next_port)
801    }
802
803    fn instantiate(
804        &self,
805        env: &mut Self::InstantiateEnv,
806        _meta: &mut Self::Meta,
807        _graph: DfirGraph,
808        extra_stmts: &[syn::Stmt],
809        sidecars: &[syn::Expr],
810    ) {
811        assert!(extra_stmts.is_empty());
812        assert!(sidecars.is_empty());
813        let service = env.CustomService(self.host.clone(), vec![]);
814        *self.underlying.borrow_mut() = Some(service);
815    }
816
817    fn update_meta(&self, _meta: &Self::Meta) {}
818}
819
820impl ExternalSpec<'_, HydroDeploy> for Arc<dyn Host> {
821    fn build(self, _key: LocationKey, _name_hint: &str) -> DeployExternal {
822        DeployExternal {
823            next_port: Rc::new(RefCell::new(0)),
824            host: self,
825            underlying: Rc::new(RefCell::new(None)),
826            allocated_ports: Rc::new(RefCell::new(HashMap::new())),
827            client_ports: Rc::new(RefCell::new(HashMap::new())),
828        }
829    }
830}
831
832impl<H: Host + 'static> ExternalSpec<'_, HydroDeploy> for Arc<H> {
833    fn build(self, _key: LocationKey, _name_hint: &str) -> DeployExternal {
834        DeployExternal {
835            next_port: Rc::new(RefCell::new(0)),
836            host: self,
837            underlying: Rc::new(RefCell::new(None)),
838            allocated_ports: Rc::new(RefCell::new(HashMap::new())),
839            client_ports: Rc::new(RefCell::new(HashMap::new())),
840        }
841    }
842}
843
844pub(crate) enum CrateOrTrybuild {
845    Crate(RustCrate, Arc<dyn Host>),
846    Trybuild(TrybuildHost),
847}
848
849#[expect(missing_docs, reason = "TODO")]
850#[derive(Clone)]
851pub struct DeployNode {
852    next_port: Rc<RefCell<usize>>,
853    service_spec: Rc<RefCell<Option<CrateOrTrybuild>>>,
854    underlying: Rc<RefCell<Option<Arc<RustCrateService>>>>,
855}
856
857impl DeployCrateWrapper for DeployNode {
858    fn underlying(&self) -> Arc<RustCrateService> {
859        Arc::clone(self.underlying.borrow().as_ref().unwrap())
860    }
861}
862
863impl Node for DeployNode {
864    type Port = String;
865    /// Map from Cluster location ID to member IDs.
866    type Meta = SparseSecondaryMap<LocationKey, Vec<TaglessMemberId>>;
867    type InstantiateEnv = Deployment;
868
869    fn next_port(&self) -> String {
870        let next_port = *self.next_port.borrow();
871        *self.next_port.borrow_mut() += 1;
872
873        format!("port_{}", next_port)
874    }
875
876    fn update_meta(&self, meta: &Self::Meta) {
877        let underlying_node = self.underlying.borrow();
878        underlying_node.as_ref().unwrap().update_meta(HydroMeta {
879            clusters: meta.clone(),
880            cluster_id: None,
881        });
882    }
883
884    fn instantiate(
885        &self,
886        env: &mut Self::InstantiateEnv,
887        _meta: &mut Self::Meta,
888        graph: DfirGraph,
889        extra_stmts: &[syn::Stmt],
890        sidecars: &[syn::Expr],
891    ) {
892        let (service, host) = match self.service_spec.borrow_mut().take().unwrap() {
893            CrateOrTrybuild::Crate(c, host) => (c, host),
894            CrateOrTrybuild::Trybuild(trybuild) => {
895                // Determine linking mode based on host target type
896                let linking_mode = if !cfg!(target_os = "windows")
897                    && trybuild.host.target_type() == hydro_deploy::HostTargetType::Local
898                    && trybuild.rustflags.is_none()
899                {
900                    // When compiling for local, prefer dynamic linking to reduce binary size
901                    // Windows is currently not supported due to https://github.com/bevyengine/bevy/pull/2016
902                    LinkingMode::Dynamic
903                } else {
904                    LinkingMode::Static
905                };
906                let (bin_name, config) = create_graph_trybuild(
907                    graph,
908                    extra_stmts,
909                    sidecars,
910                    trybuild.name_hint.as_deref(),
911                    crate::compile::trybuild::generate::DeployMode::HydroDeploy,
912                    linking_mode,
913                );
914                let host = trybuild.host.clone();
915                (
916                    create_trybuild_service(
917                        trybuild,
918                        &config.project_dir,
919                        &config.target_dir,
920                        config.features.as_deref(),
921                        &bin_name,
922                        &config.linking_mode,
923                    ),
924                    host,
925                )
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    ) {
982        let has_trybuild = self
983            .cluster_spec
984            .borrow()
985            .as_ref()
986            .unwrap()
987            .iter()
988            .any(|spec| matches!(spec, CrateOrTrybuild::Trybuild { .. }));
989
990        // For clusters, use static linking if ANY host is non-local (conservative approach)
991        let linking_mode = if !cfg!(target_os = "windows")
992            && self
993                .cluster_spec
994                .borrow()
995                .as_ref()
996                .unwrap()
997                .iter()
998                .all(|spec| match spec {
999                    CrateOrTrybuild::Crate(_, _) => true, // crates handle their own linking
1000                    CrateOrTrybuild::Trybuild(t) => {
1001                        t.host.target_type() == hydro_deploy::HostTargetType::Local
1002                            && t.rustflags.is_none()
1003                    }
1004                }) {
1005            // See comment above for Windows exception
1006            LinkingMode::Dynamic
1007        } else {
1008            LinkingMode::Static
1009        };
1010
1011        let maybe_trybuild = if has_trybuild {
1012            Some(create_graph_trybuild(
1013                graph,
1014                extra_stmts,
1015                sidecars,
1016                self.name_hint.as_deref(),
1017                crate::compile::trybuild::generate::DeployMode::HydroDeploy,
1018                linking_mode,
1019            ))
1020        } else {
1021            None
1022        };
1023
1024        let cluster_nodes = self
1025            .cluster_spec
1026            .borrow_mut()
1027            .take()
1028            .unwrap()
1029            .into_iter()
1030            .map(|spec| {
1031                let (service, host) = match spec {
1032                    CrateOrTrybuild::Crate(c, host) => (c, host),
1033                    CrateOrTrybuild::Trybuild(trybuild) => {
1034                        let (bin_name, config) = maybe_trybuild.as_ref().unwrap();
1035                        let host = trybuild.host.clone();
1036                        (
1037                            create_trybuild_service(
1038                                trybuild,
1039                                &config.project_dir,
1040                                &config.target_dir,
1041                                config.features.as_deref(),
1042                                bin_name,
1043                                &config.linking_mode,
1044                            ),
1045                            host,
1046                        )
1047                    }
1048                };
1049
1050                env.add_service(service, host)
1051            })
1052            .collect::<Vec<_>>();
1053        meta.insert(
1054            self.key,
1055            (0..(cluster_nodes.len() as u32))
1056                .map(TaglessMemberId::from_raw_id)
1057                .collect(),
1058        );
1059        *self.members.borrow_mut() = cluster_nodes
1060            .into_iter()
1061            .map(|n| DeployClusterNode { underlying: n })
1062            .collect();
1063    }
1064
1065    fn update_meta(&self, meta: &Self::Meta) {
1066        for (cluster_id, node) in self.members.borrow().iter().enumerate() {
1067            node.underlying.update_meta(HydroMeta {
1068                clusters: meta.clone(),
1069                cluster_id: Some(TaglessMemberId::from_raw_id(cluster_id as u32)),
1070            });
1071        }
1072    }
1073}
1074
1075#[expect(missing_docs, reason = "TODO")]
1076#[derive(Clone)]
1077pub struct DeployProcessSpec(RustCrate, Arc<dyn Host>);
1078
1079impl DeployProcessSpec {
1080    #[expect(missing_docs, reason = "TODO")]
1081    pub fn new(t: RustCrate, host: Arc<dyn Host>) -> Self {
1082        Self(t, host)
1083    }
1084}
1085
1086impl ProcessSpec<'_, HydroDeploy> for DeployProcessSpec {
1087    fn build(self, _key: LocationKey, _name_hint: &str) -> DeployNode {
1088        DeployNode {
1089            next_port: Rc::new(RefCell::new(0)),
1090            service_spec: Rc::new(RefCell::new(Some(CrateOrTrybuild::Crate(self.0, self.1)))),
1091            underlying: Rc::new(RefCell::new(None)),
1092        }
1093    }
1094}
1095
1096impl ProcessSpec<'_, HydroDeploy> for TrybuildHost {
1097    fn build(mut self, key: LocationKey, name_hint: &str) -> DeployNode {
1098        self.name_hint = Some(format!("{} (process {})", name_hint, key));
1099        DeployNode {
1100            next_port: Rc::new(RefCell::new(0)),
1101            service_spec: Rc::new(RefCell::new(Some(CrateOrTrybuild::Trybuild(self)))),
1102            underlying: Rc::new(RefCell::new(None)),
1103        }
1104    }
1105}
1106
1107#[expect(missing_docs, reason = "TODO")]
1108#[derive(Clone)]
1109pub struct DeployClusterSpec(Vec<(RustCrate, Arc<dyn Host>)>);
1110
1111impl DeployClusterSpec {
1112    #[expect(missing_docs, reason = "TODO")]
1113    pub fn new(crates: Vec<(RustCrate, Arc<dyn Host>)>) -> Self {
1114        Self(crates)
1115    }
1116}
1117
1118impl ClusterSpec<'_, HydroDeploy> for DeployClusterSpec {
1119    fn build(self, key: LocationKey, _name_hint: &str) -> DeployCluster {
1120        DeployCluster {
1121            key,
1122            next_port: Rc::new(RefCell::new(0)),
1123            cluster_spec: Rc::new(RefCell::new(Some(
1124                self.0
1125                    .into_iter()
1126                    .map(|(c, h)| CrateOrTrybuild::Crate(c, h))
1127                    .collect(),
1128            ))),
1129            members: Rc::new(RefCell::new(vec![])),
1130            name_hint: None,
1131        }
1132    }
1133}
1134
1135impl<T: Into<TrybuildHost>, I: IntoIterator<Item = T>> ClusterSpec<'_, HydroDeploy> for I {
1136    fn build(self, key: LocationKey, name_hint: &str) -> DeployCluster {
1137        let name_hint = format!("{} (cluster {})", name_hint, key);
1138        DeployCluster {
1139            key,
1140            next_port: Rc::new(RefCell::new(0)),
1141            cluster_spec: Rc::new(RefCell::new(Some(
1142                self.into_iter()
1143                    .enumerate()
1144                    .map(|(idx, b)| {
1145                        let mut b = b.into();
1146                        b.name_hint = Some(name_hint.clone());
1147                        b.cluster_idx = Some(idx);
1148                        CrateOrTrybuild::Trybuild(b)
1149                    })
1150                    .collect(),
1151            ))),
1152            members: Rc::new(RefCell::new(vec![])),
1153            name_hint: Some(name_hint),
1154        }
1155    }
1156}
1157
1158fn create_trybuild_service(
1159    trybuild: TrybuildHost,
1160    dir: &std::path::Path,
1161    target_dir: &std::path::PathBuf,
1162    features: Option<&[String]>,
1163    bin_name: &str,
1164    linking_mode: &LinkingMode,
1165) -> RustCrate {
1166    // For dynamic linking, use the dylib-examples crate; for static, use the base crate
1167    let crate_dir = match linking_mode {
1168        LinkingMode::Dynamic => dir.join("dylib-examples"),
1169        LinkingMode::Static => dir.to_path_buf(),
1170    };
1171
1172    let mut ret = RustCrate::new(&crate_dir, dir)
1173        .target_dir(target_dir)
1174        .example(bin_name)
1175        .no_default_features();
1176
1177    ret = ret.set_is_dylib(matches!(linking_mode, LinkingMode::Dynamic));
1178
1179    if let Some(display_name) = trybuild.display_name {
1180        ret = ret.display_name(display_name);
1181    } else if let Some(name_hint) = trybuild.name_hint {
1182        if let Some(cluster_idx) = trybuild.cluster_idx {
1183            ret = ret.display_name(format!("{} / {}", name_hint, cluster_idx));
1184        } else {
1185            ret = ret.display_name(name_hint);
1186        }
1187    }
1188
1189    if let Some(rustflags) = trybuild.rustflags {
1190        ret = ret.rustflags(rustflags);
1191    }
1192
1193    if let Some(profile) = trybuild.profile {
1194        ret = ret.profile(profile);
1195    }
1196
1197    if let Some(tracing) = trybuild.tracing {
1198        ret = ret.tracing(tracing);
1199    }
1200
1201    if let Some(core) = trybuild.pin_to_core {
1202        ret = ret.pin_to_core(core);
1203    }
1204
1205    ret = ret.features(
1206        vec!["hydro___feature_deploy_integration".to_owned()]
1207            .into_iter()
1208            .chain(
1209                trybuild
1210                    .additional_hydro_features
1211                    .into_iter()
1212                    .map(|runtime_feature| {
1213                        assert!(
1214                            HYDRO_RUNTIME_FEATURES.iter().any(|f| f == &runtime_feature),
1215                            "{runtime_feature} is not a valid Hydro runtime feature"
1216                        );
1217                        format!("hydro___feature_{runtime_feature}")
1218                    }),
1219            )
1220            .chain(trybuild.features),
1221    );
1222
1223    for (key, value) in trybuild.build_envs {
1224        ret = ret.build_env(key, value);
1225    }
1226
1227    for (key, value) in trybuild.env {
1228        ret = ret.env(key, value);
1229    }
1230
1231    ret = ret.build_env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
1232    ret = ret.config("build.incremental = false");
1233
1234    if let Some(features) = features {
1235        ret = ret.features(features);
1236    }
1237
1238    ret
1239}