@tishlang/tish 1.9.2 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/bin/tish +0 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +8 -6
  3. package/crates/js_to_tish/src/transform/stmt.rs +12 -13
  4. package/crates/tish/Cargo.toml +1 -1
  5. package/crates/tish/src/cargo_native_registry.rs +4 -1
  6. package/crates/tish/src/main.rs +11 -8
  7. package/crates/tish/tests/integration_test.rs +145 -7
  8. package/crates/tish_ast/src/ast.rs +3 -9
  9. package/crates/tish_build_utils/src/lib.rs +43 -15
  10. package/crates/tish_builtins/src/array.rs +2 -3
  11. package/crates/tish_builtins/src/construct.rs +15 -28
  12. package/crates/tish_builtins/src/globals.rs +18 -16
  13. package/crates/tish_builtins/src/helpers.rs +1 -4
  14. package/crates/tish_builtins/src/lib.rs +1 -0
  15. package/crates/tish_builtins/src/object.rs +10 -10
  16. package/crates/tish_builtins/src/string.rs +1 -3
  17. package/crates/tish_builtins/src/symbol.rs +83 -0
  18. package/crates/tish_compile/src/codegen.rs +123 -138
  19. package/crates/tish_compile/src/lib.rs +25 -3
  20. package/crates/tish_compile/src/resolve.rs +6 -3
  21. package/crates/tish_compile/src/types.rs +6 -6
  22. package/crates/tish_compile_js/src/codegen.rs +8 -5
  23. package/crates/tish_core/src/console_style.rs +9 -0
  24. package/crates/tish_core/src/json.rs +17 -7
  25. package/crates/tish_core/src/macros.rs +2 -2
  26. package/crates/tish_core/src/value.rs +192 -4
  27. package/crates/tish_cranelift_runtime/Cargo.toml +4 -0
  28. package/crates/tish_eval/src/eval.rs +135 -73
  29. package/crates/tish_eval/src/http.rs +18 -12
  30. package/crates/tish_eval/src/lib.rs +29 -0
  31. package/crates/tish_eval/src/regex.rs +1 -1
  32. package/crates/tish_eval/src/value.rs +89 -4
  33. package/crates/tish_eval/src/value_convert.rs +30 -8
  34. package/crates/tish_fmt/src/lib.rs +4 -1
  35. package/crates/tish_lexer/src/lib.rs +7 -2
  36. package/crates/tish_llvm/src/lib.rs +2 -2
  37. package/crates/tish_lsp/src/builtin_goto.rs +111 -10
  38. package/crates/tish_lsp/src/import_goto.rs +35 -22
  39. package/crates/tish_lsp/src/main.rs +118 -85
  40. package/crates/tish_native/src/build.rs +187 -10
  41. package/crates/tish_native/src/lib.rs +92 -8
  42. package/crates/tish_parser/src/lib.rs +5 -2
  43. package/crates/tish_parser/src/parser.rs +45 -75
  44. package/crates/tish_pg/src/error.rs +1 -1
  45. package/crates/tish_pg/src/lib.rs +61 -73
  46. package/crates/tish_resolve/src/lib.rs +283 -158
  47. package/crates/tish_resolve/src/pos.rs +10 -2
  48. package/crates/tish_runtime/Cargo.toml +3 -0
  49. package/crates/tish_runtime/src/http.rs +39 -39
  50. package/crates/tish_runtime/src/http_fetch.rs +12 -12
  51. package/crates/tish_runtime/src/lib.rs +26 -43
  52. package/crates/tish_runtime/src/native_promise.rs +0 -11
  53. package/crates/tish_runtime/src/promise.rs +14 -1
  54. package/crates/tish_runtime/src/promise_io.rs +1 -4
  55. package/crates/tish_runtime/src/ws.rs +40 -27
  56. package/crates/tish_runtime/tests/fetch_readable_stream.rs +10 -8
  57. package/crates/tish_ui/src/jsx.rs +6 -4
  58. package/crates/tish_ui/src/lib.rs +2 -2
  59. package/crates/tish_ui/src/runtime/hooks.rs +5 -15
  60. package/crates/tish_ui/src/runtime/mod.rs +16 -17
  61. package/crates/tish_vm/Cargo.toml +2 -0
  62. package/crates/tish_vm/src/vm.rs +218 -153
  63. package/crates/tish_wasm/src/lib.rs +33 -7
  64. package/crates/tish_wasm_runtime/Cargo.toml +4 -1
  65. package/crates/tish_wasm_runtime/src/lib.rs +2 -1
  66. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  67. package/crates/tishlang_cargo_bindgen/src/discover.rs +10 -5
  68. package/crates/tishlang_cargo_bindgen/src/infer.rs +18 -8
  69. package/crates/tishlang_cargo_bindgen/src/lib.rs +25 -26
  70. package/crates/tishlang_cargo_bindgen/src/main.rs +41 -38
  71. package/crates/tishlang_cargo_bindgen/src/metadata.rs +4 -1
  72. package/justfile +3 -3
  73. package/package.json +1 -1
  74. package/platform/darwin-arm64/tish +0 -0
  75. package/platform/darwin-x64/tish +0 -0
  76. package/platform/linux-arm64/tish +0 -0
  77. package/platform/linux-x64/tish +0 -0
  78. package/platform/win32-x64/tish.exe +0 -0
@@ -8,15 +8,15 @@ use regex::Regex;
8
8
  use tower_lsp::jsonrpc::Result;
9
9
  use tower_lsp::lsp_types::{
10
10
  CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse,
11
- CompletionTriggerKind, Diagnostic, DiagnosticSeverity, DiagnosticTag, DidChangeTextDocumentParams,
12
- DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentFormattingParams,
13
- DocumentSymbolParams, DocumentSymbolResponse, GotoDefinitionParams, GotoDefinitionResponse,
14
- Hover, HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult,
15
- Location, MarkupContent, MarkupKind, MessageType, NumberOrString, OneOf, Position, Range,
16
- ReferenceParams, RenameOptions, RenameParams, ServerCapabilities, ServerInfo,
17
- WorkDoneProgressOptions,
18
- SymbolInformation, SymbolKind, TextDocumentPositionParams, TextDocumentSyncCapability,
19
- TextDocumentSyncKind, Url, WorkspaceEdit, WorkspaceSymbolParams,
11
+ CompletionTriggerKind, Diagnostic, DiagnosticSeverity, DiagnosticTag,
12
+ DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
13
+ DocumentFormattingParams, DocumentSymbol, DocumentSymbolParams, DocumentSymbolResponse,
14
+ GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams,
15
+ HoverProviderCapability, InitializeParams, InitializeResult, Location, MarkupContent,
16
+ MarkupKind, MessageType, NumberOrString, OneOf, Position, Range, ReferenceParams,
17
+ RenameOptions, RenameParams, ServerCapabilities, ServerInfo, SymbolInformation, SymbolKind,
18
+ SymbolTag, TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, Url,
19
+ WorkDoneProgressOptions, WorkspaceEdit, WorkspaceSymbolParams,
20
20
  };
21
21
  use tower_lsp::lsp_types::{PrepareRenameResponse, TextEdit};
22
22
  use tower_lsp::{Client, LanguageServer, LspService, Server};
@@ -78,6 +78,49 @@ fn diag_range(line: u32, col: u32, text: &str) -> Range {
78
78
  }
79
79
  }
80
80
 
81
+ /// `lsp-types` still requires the `deprecated` field on these structs, but marks it
82
+ /// `#[deprecated(note = "Use tags instead")]`. Use `tags` with [`SymbolTag::Deprecated`] when a
83
+ /// symbol is actually deprecated; this helper keeps a single `#[allow(deprecated)]` boundary.
84
+ #[allow(deprecated)]
85
+ fn symbol_information(
86
+ name: String,
87
+ kind: SymbolKind,
88
+ tags: Option<Vec<SymbolTag>>,
89
+ location: Location,
90
+ container_name: Option<String>,
91
+ ) -> SymbolInformation {
92
+ SymbolInformation {
93
+ name,
94
+ kind,
95
+ tags,
96
+ deprecated: None,
97
+ location,
98
+ container_name,
99
+ }
100
+ }
101
+
102
+ #[allow(deprecated)]
103
+ fn document_symbol(
104
+ name: String,
105
+ detail: Option<String>,
106
+ kind: SymbolKind,
107
+ tags: Option<Vec<SymbolTag>>,
108
+ range: Range,
109
+ selection_range: Range,
110
+ children: Option<Vec<DocumentSymbol>>,
111
+ ) -> DocumentSymbol {
112
+ DocumentSymbol {
113
+ name,
114
+ detail,
115
+ kind,
116
+ tags,
117
+ deprecated: None,
118
+ range,
119
+ selection_range,
120
+ children,
121
+ }
122
+ }
123
+
81
124
  fn publish_parse_and_lint(client: &Client, uri: Url, text: &str) {
82
125
  let mut diags = Vec::new();
83
126
  match tishlang_parser::parse(text) {
@@ -115,10 +158,7 @@ fn publish_parse_and_lint(client: &Client, uri: Url, text: &str) {
115
158
  "tish-unused-parameter",
116
159
  ),
117
160
  tishlang_resolve::UnusedBindingKind::Variable => (
118
- format!(
119
- "`{}` is declared but its value is never read",
120
- ub.name
121
- ),
161
+ format!("`{}` is declared but its value is never read", ub.name),
122
162
  "tish-unused-variable",
123
163
  ),
124
164
  };
@@ -317,7 +357,7 @@ impl LanguageServer for Backend {
317
357
  return Ok(None);
318
358
  };
319
359
 
320
- let mut syms: Vec<tower_lsp::lsp_types::DocumentSymbol> = Vec::new();
360
+ let mut syms: Vec<DocumentSymbol> = Vec::new();
321
361
  for s in &program.statements {
322
362
  doc_symbol_stmt(s, &text, &mut syms);
323
363
  }
@@ -432,7 +472,9 @@ impl LanguageServer for Backend {
432
472
  }
433
473
  None => {
434
474
  if tishlang_resolve::is_runtime_global_ident(use_site.name.as_ref()) {
435
- md.push_str("\n\n_Interpreter root global (no lexical declaration in this file)._");
475
+ md.push_str(
476
+ "\n\n_Interpreter root global (no lexical declaration in this file)._",
477
+ );
436
478
  let word = word_at_position(&text, pos);
437
479
  if !word.is_empty() {
438
480
  if let Some(root) = self.tishlang_source_root.read().unwrap().clone() {
@@ -583,12 +625,8 @@ impl LanguageServer for Backend {
583
625
  else {
584
626
  return Ok(None);
585
627
  };
586
- let spans = tishlang_resolve::reference_spans_for_def(
587
- &program,
588
- &text,
589
- nu.name.as_ref(),
590
- def,
591
- );
628
+ let spans =
629
+ tishlang_resolve::reference_spans_for_def(&program, &text, nu.name.as_ref(), def);
592
630
  let mut edits: Vec<TextEdit> = spans
593
631
  .into_iter()
594
632
  .map(|sp| TextEdit {
@@ -598,10 +636,8 @@ impl LanguageServer for Backend {
598
636
  .collect();
599
637
  // Apply from end of document so earlier ranges stay valid when lengths change.
600
638
  edits.sort_by(|a, b| {
601
- (b.range.start.line, b.range.start.character).cmp(&(
602
- a.range.start.line,
603
- a.range.start.character,
604
- ))
639
+ (b.range.start.line, b.range.start.character)
640
+ .cmp(&(a.range.start.line, a.range.start.character))
605
641
  });
606
642
  let mut m = HashMap::new();
607
643
  m.insert(uri, edits);
@@ -689,41 +725,35 @@ fn collect_workspace_syms(
689
725
  ) {
690
726
  match s {
691
727
  tishlang_ast::Statement::FunDecl {
692
- name,
693
- name_span,
694
- ..
728
+ name, name_span, ..
695
729
  } => {
696
730
  if name.to_lowercase().contains(query) {
697
- out.push(SymbolInformation {
698
- name: name.to_string(),
699
- kind: SymbolKind::FUNCTION,
700
- tags: None,
701
- deprecated: None,
702
- location: Location {
731
+ out.push(symbol_information(
732
+ name.to_string(),
733
+ SymbolKind::FUNCTION,
734
+ None,
735
+ Location {
703
736
  uri: uri.clone(),
704
737
  range: span_to_range(name_span, text),
705
738
  },
706
- container_name: None,
707
- });
739
+ None,
740
+ ));
708
741
  }
709
742
  }
710
743
  tishlang_ast::Statement::VarDecl {
711
- name,
712
- name_span,
713
- ..
744
+ name, name_span, ..
714
745
  } => {
715
746
  if name.to_lowercase().contains(query) {
716
- out.push(SymbolInformation {
717
- name: name.to_string(),
718
- kind: SymbolKind::VARIABLE,
719
- tags: None,
720
- deprecated: None,
721
- location: Location {
747
+ out.push(symbol_information(
748
+ name.to_string(),
749
+ SymbolKind::VARIABLE,
750
+ None,
751
+ Location {
722
752
  uri: uri.clone(),
723
753
  range: span_to_range(name_span, text),
724
754
  },
725
- container_name: None,
726
- });
755
+ None,
756
+ ));
727
757
  }
728
758
  }
729
759
  tishlang_ast::Statement::Block { statements, .. } => {
@@ -744,9 +774,7 @@ pub(crate) fn find_export(
744
774
  for s in &program.statements {
745
775
  match s {
746
776
  tishlang_ast::Statement::FunDecl {
747
- name: n,
748
- name_span,
749
- ..
777
+ name: n, name_span, ..
750
778
  } if n.as_ref() == name => {
751
779
  return Some(Location {
752
780
  uri: uri.clone(),
@@ -754,9 +782,7 @@ pub(crate) fn find_export(
754
782
  });
755
783
  }
756
784
  tishlang_ast::Statement::VarDecl {
757
- name: n,
758
- name_span,
759
- ..
785
+ name: n, name_span, ..
760
786
  } if n.as_ref() == name => {
761
787
  return Some(Location {
762
788
  uri: uri.clone(),
@@ -785,17 +811,13 @@ fn find_decl_in_stmt(
785
811
  ) -> Option<Location> {
786
812
  match s {
787
813
  tishlang_ast::Statement::FunDecl {
788
- name,
789
- name_span,
790
- ..
814
+ name, name_span, ..
791
815
  } if name.as_ref() == word => Some(Location {
792
816
  uri: uri.clone(),
793
817
  range: span_to_range(name_span, text),
794
818
  }),
795
819
  tishlang_ast::Statement::VarDecl {
796
- name,
797
- name_span,
798
- ..
820
+ name, name_span, ..
799
821
  } if name.as_ref() == word => Some(Location {
800
822
  uri: uri.clone(),
801
823
  range: span_to_range(name_span, text),
@@ -915,15 +937,26 @@ fn value_completion_kind_stmt(
915
937
  finally_body,
916
938
  ..
917
939
  } => value_completion_kind_stmt(body, name)
918
- .or_else(|| catch_body.as_ref().and_then(|b| value_completion_kind_stmt(b, name)))
919
- .or_else(|| finally_body.as_ref().and_then(|b| value_completion_kind_stmt(b, name))),
940
+ .or_else(|| {
941
+ catch_body
942
+ .as_ref()
943
+ .and_then(|b| value_completion_kind_stmt(b, name))
944
+ })
945
+ .or_else(|| {
946
+ finally_body
947
+ .as_ref()
948
+ .and_then(|b| value_completion_kind_stmt(b, name))
949
+ }),
920
950
  tishlang_ast::Statement::Switch {
921
951
  cases,
922
952
  default_body,
923
953
  ..
924
954
  } => {
925
955
  for (_e, stmts) in cases {
926
- if let Some(k) = stmts.iter().find_map(|st| value_completion_kind_stmt(st, name)) {
956
+ if let Some(k) = stmts
957
+ .iter()
958
+ .find_map(|st| value_completion_kind_stmt(st, name))
959
+ {
927
960
  return Some(k);
928
961
  }
929
962
  }
@@ -934,7 +967,9 @@ fn value_completion_kind_stmt(
934
967
  })
935
968
  }
936
969
  tishlang_ast::Statement::Export { declaration, .. } => match declaration.as_ref() {
937
- tishlang_ast::ExportDeclaration::Named(inner) => value_completion_kind_stmt(inner, name),
970
+ tishlang_ast::ExportDeclaration::Named(inner) => {
971
+ value_completion_kind_stmt(inner, name)
972
+ }
938
973
  tishlang_ast::ExportDeclaration::Default(_) => None,
939
974
  },
940
975
  _ => None,
@@ -944,7 +979,7 @@ fn value_completion_kind_stmt(
944
979
  fn doc_symbol_stmt(
945
980
  s: &tishlang_ast::Statement,
946
981
  text: &str,
947
- out: &mut Vec<tower_lsp::lsp_types::DocumentSymbol>,
982
+ out: &mut Vec<DocumentSymbol>,
948
983
  ) {
949
984
  match s {
950
985
  tishlang_ast::Statement::FunDecl {
@@ -956,20 +991,19 @@ fn doc_symbol_stmt(
956
991
  } => {
957
992
  let mut children = Vec::new();
958
993
  collect_child_syms(body, text, &mut children);
959
- out.push(tower_lsp::lsp_types::DocumentSymbol {
960
- name: name.to_string(),
961
- detail: None,
962
- kind: tower_lsp::lsp_types::SymbolKind::FUNCTION,
963
- tags: None,
964
- deprecated: None,
965
- range: span_to_range(span, text),
966
- selection_range: span_to_range(name_span, text),
967
- children: if children.is_empty() {
994
+ out.push(document_symbol(
995
+ name.to_string(),
996
+ None,
997
+ SymbolKind::FUNCTION,
998
+ None,
999
+ span_to_range(span, text),
1000
+ span_to_range(name_span, text),
1001
+ if children.is_empty() {
968
1002
  None
969
1003
  } else {
970
1004
  Some(children)
971
1005
  },
972
- });
1006
+ ));
973
1007
  }
974
1008
  tishlang_ast::Statement::VarDecl {
975
1009
  name,
@@ -977,16 +1011,15 @@ fn doc_symbol_stmt(
977
1011
  span,
978
1012
  ..
979
1013
  } => {
980
- out.push(tower_lsp::lsp_types::DocumentSymbol {
981
- name: name.to_string(),
982
- detail: None,
983
- kind: tower_lsp::lsp_types::SymbolKind::VARIABLE,
984
- tags: None,
985
- deprecated: None,
986
- range: span_to_range(span, text),
987
- selection_range: span_to_range(name_span, text),
988
- children: None,
989
- });
1014
+ out.push(document_symbol(
1015
+ name.to_string(),
1016
+ None,
1017
+ SymbolKind::VARIABLE,
1018
+ None,
1019
+ span_to_range(span, text),
1020
+ span_to_range(name_span, text),
1021
+ None,
1022
+ ));
990
1023
  }
991
1024
  tishlang_ast::Statement::Block { statements, .. } => {
992
1025
  for x in statements {
@@ -1000,7 +1033,7 @@ fn doc_symbol_stmt(
1000
1033
  fn collect_child_syms(
1001
1034
  s: &tishlang_ast::Statement,
1002
1035
  text: &str,
1003
- out: &mut Vec<tower_lsp::lsp_types::DocumentSymbol>,
1036
+ out: &mut Vec<DocumentSymbol>,
1004
1037
  ) {
1005
1038
  match s {
1006
1039
  tishlang_ast::Statement::Block { statements, .. } => {
@@ -6,8 +6,15 @@ use std::path::Path;
6
6
  use tishlang_compile::ResolvedNativeModule;
7
7
 
8
8
  /// `tishlang_runtime` Cargo feature names (subset of CLI / compile feature names).
9
- const RUNTIME_CARGO_FEATURES: &[&str] =
10
- &["http", "http-hyper", "http-io-uring", "fs", "process", "regex", "ws"];
9
+ const RUNTIME_CARGO_FEATURES: &[&str] = &[
10
+ "http",
11
+ "http-hyper",
12
+ "http-io-uring",
13
+ "fs",
14
+ "process",
15
+ "regex",
16
+ "ws",
17
+ ];
11
18
 
12
19
  /// Map CLI/compile features to flags passed to `tishlang_runtime` in the temp crate's Cargo.toml.
13
20
  /// `full` enables every optional runtime capability (matches `tish build --feature full` / LANGUAGE.md).
@@ -29,6 +36,29 @@ fn runtime_features_for_cargo(features: &[String]) -> Vec<String> {
29
36
  out
30
37
  }
31
38
 
39
+ /// `[profile.release]` for nested `cargo build` of generated crates.
40
+ fn nested_release_profile_toml() -> &'static str {
41
+ if std::env::var("TISH_FAST_NATIVE_BUILD").as_deref() == Ok("1") {
42
+ r#"[profile.release]
43
+ opt-level = 1
44
+ lto = false
45
+ codegen-units = 16
46
+ incremental = true
47
+ strip = false
48
+ debug = 0
49
+ panic = "abort"
50
+ "#
51
+ } else {
52
+ r#"[profile.release]
53
+ # Reduce binary size: strip symbols, abort on panic (no unwinding), single codegen unit
54
+ strip = true
55
+ panic = "abort"
56
+ codegen-units = 1
57
+ lto = "fat"
58
+ "#
59
+ }
60
+ }
61
+
32
62
  /// Inject `mod generated_native;` after the crate attribute so the binary crate can call `crate::generated_native::…`.
33
63
  fn inject_generated_native_mod(rust_code: &str) -> String {
34
64
  if let Some(pos) = rust_code.find("\n\n") {
@@ -108,6 +138,7 @@ pub fn build_via_cargo(
108
138
  String::new()
109
139
  };
110
140
 
141
+ let profile = nested_release_profile_toml();
111
142
  let cargo_toml = format!(
112
143
  r#"[package]
113
144
  name = "tish_output"
@@ -118,17 +149,12 @@ edition = "2021"
118
149
  name = "{}"
119
150
  path = "src/main.rs"
120
151
 
121
- [profile.release]
122
- # Reduce binary size: strip symbols, abort on panic (no unwinding), single codegen unit
123
- strip = true
124
- panic = "abort"
125
- codegen-units = 1
126
- lto = "fat"
152
+ {}
127
153
 
128
154
  [dependencies]
129
155
  tishlang_runtime = {{ path = {:?}{} }}
130
156
  {}{}"#,
131
- out_name, runtime_path, features_str, more_deps, ui_dep
157
+ out_name, profile, runtime_path, features_str, more_deps, ui_dep
132
158
  );
133
159
 
134
160
  fs::write(build_dir.join("Cargo.toml"), cargo_toml)
@@ -159,6 +185,155 @@ tishlang_runtime = {{ path = {:?}{} }}
159
185
  Ok(())
160
186
  }
161
187
 
188
+ /// Build several native binaries in **one** nested Cargo project (shared `tishlang_runtime` compile).
189
+ ///
190
+ /// `bins` order must match `outputs`: each `(stem, rust_code, generated_native_rs)` pairs with
191
+ /// `outputs[i].0` (entry path — used only for validation) and `outputs[i].1` (final binary path).
192
+ pub(crate) fn build_many_via_cargo(
193
+ bins: Vec<(String, String, Option<String>)>,
194
+ native_modules: Vec<ResolvedNativeModule>,
195
+ features: &[String],
196
+ extra_dependencies_toml: &str,
197
+ needs_tokio: bool,
198
+ needs_ui: bool,
199
+ outputs: &[(&Path, &Path)],
200
+ project_root: Option<&Path>,
201
+ ) -> Result<(), String> {
202
+ if bins.len() != outputs.len() {
203
+ return Err(format!(
204
+ "build_many_via_cargo: bins ({}) != outputs ({})",
205
+ bins.len(),
206
+ outputs.len()
207
+ ));
208
+ }
209
+ for (i, (stem, _, _)) in bins.iter().enumerate() {
210
+ let entry = outputs[i].0;
211
+ let expect = entry.file_stem().and_then(|s| s.to_str()).unwrap_or("");
212
+ if expect != stem {
213
+ return Err(format!(
214
+ "build_many_via_cargo: stem mismatch at {}: {} vs {}",
215
+ i, stem, expect
216
+ ));
217
+ }
218
+ }
219
+
220
+ let batch_id = format!("many_{}", std::process::id());
221
+ let build_dir = tishlang_build_utils::create_build_dir("tish_build_many", &batch_id)?;
222
+
223
+ let runtime_path = tishlang_build_utils::find_runtime_path_for_project(project_root)?;
224
+
225
+ let runtime_features = runtime_features_for_cargo(features);
226
+ let runtime_refs: Vec<&str> = runtime_features.iter().map(String::as_str).collect();
227
+ let features_str = if runtime_refs.is_empty() {
228
+ String::new()
229
+ } else {
230
+ format!(", features = {:?}", runtime_refs)
231
+ };
232
+
233
+ let tokio_dep = if needs_tokio {
234
+ "\ntokio = { version = \"1\", features = [\"rt-multi-thread\", \"macros\"] }\n"
235
+ } else {
236
+ ""
237
+ };
238
+
239
+ let native_deps: String = native_modules
240
+ .iter()
241
+ .filter(|m| m.use_path_dependency)
242
+ .map(|m| {
243
+ let path = m.crate_path.display().to_string().replace('\\', "/");
244
+ format!("{} = {{ path = {:?} }}\n", m.package_name, path)
245
+ })
246
+ .collect();
247
+
248
+ let mut more_deps = String::new();
249
+ more_deps.push_str(tokio_dep);
250
+ if !native_deps.is_empty() {
251
+ more_deps.push_str(&format!("\n{}", native_deps));
252
+ }
253
+ if !extra_dependencies_toml.trim().is_empty() {
254
+ more_deps.push_str(&format!("\n{}", extra_dependencies_toml));
255
+ }
256
+
257
+ let tish_ui_path = std::path::Path::new(&runtime_path)
258
+ .parent()
259
+ .ok_or_else(|| "invalid tishlang_runtime path (no parent)".to_string())?
260
+ .join("tish_ui");
261
+ let ui_dep = if needs_ui {
262
+ format!(
263
+ "\ntishlang_ui = {{ path = {:?}, default-features = false, features = [\"runtime\"] }}\n",
264
+ tish_ui_path.display().to_string().replace('\\', "/")
265
+ )
266
+ } else {
267
+ String::new()
268
+ };
269
+
270
+ let mut bin_tables = String::new();
271
+ for (stem, rust_code, generated_native_rs) in &bins {
272
+ let bin_dir = build_dir.join("src/bin").join(stem);
273
+ fs::create_dir_all(&bin_dir).map_err(|e| format!("create bin dir: {}", e))?;
274
+
275
+ let rust_main = if generated_native_rs.is_some() {
276
+ inject_generated_native_mod(rust_code)
277
+ } else {
278
+ rust_code.clone()
279
+ };
280
+
281
+ fs::write(bin_dir.join("main.rs"), rust_main)
282
+ .map_err(|e| format!("write main.rs for {}: {}", stem, e))?;
283
+ if let Some(gen) = generated_native_rs {
284
+ fs::write(bin_dir.join("generated_native.rs"), gen)
285
+ .map_err(|e| format!("write generated_native.rs for {}: {}", stem, e))?;
286
+ }
287
+
288
+ bin_tables.push_str(&format!(
289
+ r#"[[bin]]
290
+ name = "{stem}"
291
+ path = "src/bin/{stem}/main.rs"
292
+
293
+ "#
294
+ ));
295
+ }
296
+
297
+ let profile = nested_release_profile_toml();
298
+ let cargo_toml = format!(
299
+ r#"[package]
300
+ name = "tish_output_many"
301
+ version = "0.1.0"
302
+ edition = "2021"
303
+
304
+ {}{}
305
+ [dependencies]
306
+ tishlang_runtime = {{ path = {:?}{} }}
307
+ {}{}"#,
308
+ bin_tables, profile, runtime_path, features_str, more_deps, ui_dep
309
+ );
310
+
311
+ fs::write(build_dir.join("Cargo.toml"), cargo_toml)
312
+ .map_err(|e| format!("Cannot write Cargo.toml: {}", e))?;
313
+
314
+ let workspace_target = Path::new(&runtime_path)
315
+ .parent()
316
+ .and_then(|p| p.parent())
317
+ .map(|ws| ws.join("target"));
318
+ let target_dir = workspace_target.filter(|p| p.exists());
319
+ let binary_dir = target_dir
320
+ .as_ref()
321
+ .map(|t| t.join("release"))
322
+ .unwrap_or_else(|| build_dir.join("target").join("release"));
323
+
324
+ tishlang_build_utils::run_cargo_build(&build_dir, target_dir.as_deref())?;
325
+
326
+ for i in 0..bins.len() {
327
+ let stem = bins[i].0.as_str();
328
+ let output_path = outputs[i].1;
329
+ let binary = tishlang_build_utils::find_release_binary(binary_dir.as_path(), stem)?;
330
+ let target = tishlang_build_utils::resolve_output_path(output_path, stem);
331
+ tishlang_build_utils::copy_binary_to_output(&binary, &target)?;
332
+ }
333
+
334
+ Ok(())
335
+ }
336
+
162
337
  #[cfg(test)]
163
338
  mod tests {
164
339
  use super::runtime_features_for_cargo;
@@ -176,6 +351,8 @@ mod tests {
176
351
  #[test]
177
352
  fn runtime_features_merges_full_and_specific() {
178
353
  let f = runtime_features_for_cargo(&["full".to_string(), "http".to_string()]);
179
- assert_eq!(f.len(), 5);
354
+ // `full` expands to every RUNTIME_CARGO_FEATURES entry; redundant `http` must not duplicate.
355
+ assert_eq!(f.len(), super::RUNTIME_CARGO_FEATURES.len());
356
+ assert_eq!(f.iter().filter(|x| *x == "http").count(), 1);
180
357
  }
181
358
  }