1use 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
41pub enum HydroDeploy {}
46
47impl<'a> Deploy<'a> for HydroDeploy {
48 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 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 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 let linking_mode = if !cfg!(target_os = "windows")
906 && trybuild.host.target_type() == hydro_deploy::HostTargetType::Local
907 && trybuild.rustflags.is_none()
908 {
909 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 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 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, CrateOrTrybuild::Trybuild(t) => {
1002 t.host.target_type() == hydro_deploy::HostTargetType::Local
1003 && t.rustflags.is_none()
1004 }
1005 }) {
1006 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 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}