feat: nyash.toml自動解決とWindows/Python対応
- Windows向け自動ライブラリパス解決(lib接頭辞除去) - Pythonプラグイン実装改善(evalR/importRメソッド追加) - nyash.tomlに新[plugins]セクション対応開始 - プラグイン検索パスの柔軟な解決 - AOT設定Box改善とエラーハンドリング強化 - Phase 10.5bドキュメント追加(ネイティブビルド統合計画) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -41,7 +41,7 @@ Phase 10.10 は完了(DoD確認済)。アーキテクチャ転回:JITは
|
||||
|
||||
---
|
||||
|
||||
## 2025-08-29 PM3 再起動スナップショット(Strict/分離確定版)
|
||||
## 2025-08-29 PM3 再起動スナップショット(Strict/分離・ネイティブ基盤固め・Python準備)
|
||||
|
||||
### 現在の着地(Strict準備済み)
|
||||
- InvokePolicy/Observe を導入し、Lowerer の分岐をスリム化
|
||||
@ -107,10 +107,18 @@ NYASH_JIT_EVENTS_PATH=jit_events.jsonl \
|
||||
```
|
||||
|
||||
### これからの実装(優先順)
|
||||
1) 算術/比較 emit の穴埋め(Strictで落ちる箇所を優先)
|
||||
2) String RO の必要最小を policy に追加(過剰に増やさない)
|
||||
3) 追加サンプルは最小限(回帰用の小粒のみ)
|
||||
4) 必要に応じて Strict 診断のJSONイベントを最小追加(compile-fail時)
|
||||
1) ネイティブ基盤の仕上げ(10.5b)
|
||||
- `tools/build_aot.{sh,ps1}` の導線統一、Windows clang/cl内蔵化の検討
|
||||
- プラグイン解決の安定(拡張子変換/lib剥がし/検索パス/警告整備)
|
||||
2) プラグイン仕様分離(中央=nyash.toml / 各プラグイン=nyash_box.toml)
|
||||
- Loaderが `plugins/<name>/nyash_box.toml` を読み、type_id/メソッドIDを反映
|
||||
- 旧[libraries]も後方互換で維持(当面)
|
||||
3) Python統合(10.5c)
|
||||
- PyRuntimeBox/PyObjectBox のRO経路(eval/import/getattr/call/str)をVM/EXEで安定
|
||||
- autodecode/エラー伝搬の強化、WindowsでのDLL探索(PYTHONHOME/PATH)
|
||||
4) 観測・サンプル
|
||||
- EXEの `Result:` 統一、VM/EXEスモークのGreen化
|
||||
- 追加サンプルは最小限(回帰用の小粒のみ)
|
||||
|
||||
### 現在の達成状況(✅)
|
||||
- ✅ static box メソッドのMIR関数化に成功
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
# 10.5b – ネイティブビルド基盤の固め(AOT/EXE)
|
||||
|
||||
Python統合を本格化する前に、配布可能なネイティブ実行ファイル(EXE)の足回りを先に完成させる。JITは実行エンジンから外し、EXE生成専用のコンパイラとして運用する。
|
||||
|
||||
## 🎯 目的
|
||||
- VM=実行、JIT=EXE(AOT)の二系統を明確化(フォールバックなし/Fail-Fast)
|
||||
- CLIF→.o→`libnyrt`リンク→EXEのパイプラインを実効化
|
||||
- プラグイン解決をクロスプラットフォームに(.so/.dll/.dylib、自動lib剥がし、検索パス)
|
||||
- Windowsを含む実用的な配布体験を整備
|
||||
|
||||
## 🧩 範囲
|
||||
- JIT分離・Strict運用(Fail-Fast/No-fallback)
|
||||
- AOTパイプライン: `--compile-native` と `tools/build_aot.{sh,ps1}`
|
||||
- プラグインローダの拡張: 拡張子変換/`lib`剥がし、`plugin_paths`+`NYASH_PLUGIN_PATHS`
|
||||
- Windowsリンク: clang優先(`nyrt.lib`/`libnyrt.a`両対応)、bash+cc fallback
|
||||
- 観測/EXE出力の統一: `Result: <val>`、終了コード=<val>
|
||||
|
||||
## ✅ 成果(DoD)
|
||||
- `cargo build --release --features cranelift-jit` の後、
|
||||
- Linux: `./tools/build_aot.sh examples/aot_min_string_len.nyash -o app && ./app`
|
||||
- Windows: `powershell -ExecutionPolicy Bypass -File tools\build_aot.ps1 -Input examples\aot_min_string_len.nyash -Out app.exe && .\app.exe`
|
||||
- プラグインは `.so` 記述でも各OSで自動解決(.dll/.dylib へ変換、lib剥がし)
|
||||
- `tools/smoke_aot_vs_vm.sh` で VM/EXE の `Result:` 行比較が可能(差異は警告表示)
|
||||
|
||||
## 🔧 実装メモ
|
||||
- `src/runtime/plugin_loader_v2.rs` に `resolve_library_path()` を追加:
|
||||
- OS別拡張子、Windowsの`lib`剥がし、`plugin_paths`探索
|
||||
- `src/config/nyash_toml_v2.rs` に `NYASH_PLUGIN_PATHS` を追加(`;`/`:`区切り)
|
||||
- `AotConfigBox` に `set_plugin_paths()` 追加(env同期)
|
||||
- `crates/nyrt` の EXE出力統一(`Result:`/exit code)
|
||||
- Windows: `tools/build_aot.ps1`(clang→bash fallback)、Linux: `tools/build_aot.sh`
|
||||
|
||||
## 📌 次(10.5c 以降)
|
||||
- PyRuntimeBox/PyObjectBox(RO優先)
|
||||
- Python ABIルータを `libnyrt` に同梱(type_id→invokeディスパッチ)
|
||||
- 配布用パッケージ整備(nyash.toml/プラグイン配置ガイドの最終化)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# Phase 10.5 – Python ネイティブ統合(Embedding & FFI)/ JIT分離(EXE専用化)
|
||||
*(旧10.1の一部を後段フェーズに再編。Everything is Plugin/AOTの基盤上で実現)*
|
||||
# Phase 10.5 – ネイティブ基盤固め + Python ネイティブ統合
|
||||
*(旧10.1の一部を後段フェーズに再編。まずネイティブ/AOT基盤を固め、その上でPythonを統合する方針に整理)*
|
||||
|
||||
本フェーズでは方針を明確化する:実行はVMが唯一の基準系、JITは「EXE/AOT生成専用のコンパイラ」として分離運用する。
|
||||
|
||||
@ -12,10 +12,10 @@
|
||||
- 役割分担の明確化: VM=仕様/挙動の唯一の基準、JIT=ネイティブ生成器。
|
||||
- プラグイン整合: VM/EXEとも同一のBID/FFIプラグインを利用(Everything is Plugin)。
|
||||
|
||||
## 📂 サブフェーズ構成(10.5a → 10.5e)
|
||||
## 📂 サブフェーズ構成(10.5s → 10.5e)
|
||||
|
||||
先行タスク(最優先)
|
||||
- 10.5s JIT Strict/分離の確定(Fail-Fast / ノーフォールバック)
|
||||
- 10.5s JIT Strict/分離の確定(Fail-Fast / ノーフォールバック) [DONE]
|
||||
- 目的: 「VM=実行・JIT=コンパイル」の二系統で混在を排除し、検証を単純化
|
||||
- 仕様:
|
||||
- JITは実行経路から外し、`--compile-native`(AOT)でのみ使用
|
||||
@ -25,7 +25,7 @@
|
||||
- CLIに `--compile-native` を追加し、OBJ/EXE生成が一発で通る
|
||||
- VM実行は常にVMのみ(JITディスパッチ既定OFF)。
|
||||
|
||||
### 10.5a 設計・ABI整合(1–2日)
|
||||
### 10.5a(Python)設計・ABI整合(1–2日)
|
||||
- ルート選択:
|
||||
- Embedding: NyashプロセスにCPythonを埋め込み、PyObject*をハンドル管理
|
||||
- Extending: Python拡張モジュール(nyashrt)を提供し、PythonからNyashを呼ぶ
|
||||
@ -34,7 +34,20 @@
|
||||
- 変換: Nyash ⇄ Python で Bool/I64/String/Bytes/Handle を相互変換
|
||||
- GIL: birth/invoke/decRef中はGIL確保。AOTでも同等
|
||||
|
||||
### 10.5b PyRuntimeBox / PyObjectBox 実装(3–5日)
|
||||
### 10.5b ネイティブビルド基盤の固め(AOT/EXE)(2–4日)
|
||||
- 目的: Python統合の前に、AOT/EXE配布体験・クロスプラットフォーム実行の足回りを先に完成させる
|
||||
- 範囲:
|
||||
- VMとJITの分離(JIT=EXE専用)とStrict運用の徹底
|
||||
- AOTパイプラインの実働(CLIF→.o→libnyrtリンク→EXE)
|
||||
- プラグイン解決のクロスプラットフォーム化(.so/.dll/.dylib、自動lib剥がし、検索パス)
|
||||
- Windowsビルド/リンク(clang優先、MSYS2/WSL fallback)
|
||||
- EXE出力の統一(`Result: <val>`)とスモークテスト
|
||||
- DoD:
|
||||
- Linux/Windowsで `--compile-native` が通り、`plugins/` のDLL/so/dylibを自動解決
|
||||
- `tools/build_aot.{sh,ps1}` で配布しやすいEXEが生成される
|
||||
- `tools/smoke_aot_vs_vm.sh` でVM/EXEの出力照合が可能
|
||||
|
||||
### 10.5c PyRuntimeBox / PyObjectBox 実装(3–5日)
|
||||
- `PyRuntimeBox`(シングルトン): `eval(code) -> Handle` / `import(name) -> Handle`
|
||||
- `PyObjectBox`: `getattr(name) -> Handle` / `call(args...) -> Handle` / `str() -> String`
|
||||
- 参照管理: `Py_INCREF`/`Py_DECREF` をBoxライフサイクル(fini)に接続
|
||||
|
||||
@ -79,10 +79,44 @@ cat src/lib.rs
|
||||
|
||||
### 3. **Configure Your Plugin**
|
||||
```bash
|
||||
# nyash.tomlで設定
|
||||
cat nyash.toml # 実際の設定形式を確認
|
||||
# 新スタイル(推奨): 中央=nyash.toml(レジストリ最小) + 各プラグイン=nyash_box.toml(仕様書)
|
||||
cat nyash.toml
|
||||
cat plugins/<your-plugin>/nyash_box.toml
|
||||
```
|
||||
|
||||
中央の `nyash.toml` 例(抜粋)
|
||||
```toml
|
||||
[plugins]
|
||||
"libnyash_filebox_plugin" = "./plugins/nyash-filebox-plugin"
|
||||
|
||||
[plugin_paths]
|
||||
search_paths = ["./plugins/*/target/release", "./plugins/*/target/debug"]
|
||||
|
||||
[box_types]
|
||||
FileBox = 6
|
||||
```
|
||||
|
||||
各プラグインの `nyash_box.toml` 例(抜粋)
|
||||
```toml
|
||||
[box]
|
||||
name = "FileBox"
|
||||
version = "1.0.0"
|
||||
description = "File I/O operations Box"
|
||||
|
||||
[provides]
|
||||
boxes = ["FileBox"]
|
||||
|
||||
[FileBox]
|
||||
type_id = 6
|
||||
|
||||
[FileBox.methods.open]
|
||||
id = 1
|
||||
args = [ { name = "path", type = "string" }, { name = "mode", type = "string", default = "r" } ]
|
||||
returns = { type = "void", error = "string" }
|
||||
```
|
||||
|
||||
ロード時は `nyash_box.toml` が優先参照され、OS差(.so/.dll/.dylib、libプリフィックス)は自動吸収されます。従来の `[libraries]` 設定も当面は後方互換で有効です。
|
||||
|
||||
### 4. **Test Your Plugin**
|
||||
```bash
|
||||
# プラグインテスターで確認
|
||||
|
||||
@ -106,26 +106,35 @@ struct CPython {
|
||||
static CPY: Lazy<Mutex<Option<CPython>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
fn try_load_cpython() -> Result<(), ()> {
|
||||
let candidates = [
|
||||
let mut candidates: Vec<String> = vec![
|
||||
// Linux/WSL common
|
||||
"libpython3.12.so",
|
||||
"libpython3.12.so.1.0",
|
||||
"libpython3.11.so",
|
||||
"libpython3.11.so.1.0",
|
||||
"libpython3.10.so",
|
||||
"libpython3.10.so.1.0",
|
||||
"libpython3.9.so",
|
||||
"libpython3.9.so.1.0",
|
||||
"libpython3.12.so".into(),
|
||||
"libpython3.12.so.1.0".into(),
|
||||
"libpython3.11.so".into(),
|
||||
"libpython3.11.so.1.0".into(),
|
||||
"libpython3.10.so".into(),
|
||||
"libpython3.10.so.1.0".into(),
|
||||
"libpython3.9.so".into(),
|
||||
"libpython3.9.so.1.0".into(),
|
||||
// macOS
|
||||
"libpython3.12.dylib",
|
||||
"libpython3.11.dylib",
|
||||
"libpython3.10.dylib",
|
||||
"libpython3.9.dylib",
|
||||
// Windows (not targeted in 10.5b)
|
||||
// "python312.dll", "python311.dll",
|
||||
"libpython3.12.dylib".into(),
|
||||
"libpython3.11.dylib".into(),
|
||||
"libpython3.10.dylib".into(),
|
||||
"libpython3.9.dylib".into(),
|
||||
];
|
||||
for name in candidates {
|
||||
if let Ok(lib) = unsafe { Library::new(name) } {
|
||||
// Windows DLLs (search via PATH / System32)
|
||||
if cfg!(target_os = "windows") {
|
||||
let dlls = ["python312.dll","python311.dll","python310.dll","python39.dll"];
|
||||
for d in dlls.iter() { candidates.push((*d).into()); }
|
||||
if let Ok(pyhome) = std::env::var("PYTHONHOME") {
|
||||
for d in dlls.iter() {
|
||||
let p = std::path::Path::new(&pyhome).join(d);
|
||||
if p.exists() { candidates.push(p.to_string_lossy().to_string()); }
|
||||
}
|
||||
}
|
||||
}
|
||||
for name in candidates.into_iter() {
|
||||
if let Ok(lib) = unsafe { Library::new(&name) } {
|
||||
unsafe {
|
||||
let Py_Initialize = *lib.get::<unsafe extern "C" fn()>(b"Py_Initialize\0").map_err(|_| ())?;
|
||||
let Py_Finalize = *lib.get::<unsafe extern "C" fn()>(b"Py_Finalize\0").map_err(|_| ())?;
|
||||
|
||||
@ -7,9 +7,10 @@ pub struct AotConfigBox {
|
||||
// staging fields (apply() writes to env)
|
||||
pub output_file: Option<String>,
|
||||
pub emit_obj_out: Option<String>,
|
||||
pub plugin_paths: Option<String>,
|
||||
}
|
||||
|
||||
impl AotConfigBox { pub fn new() -> Self { Self { base: BoxBase::new(), output_file: None, emit_obj_out: None } } }
|
||||
impl AotConfigBox { pub fn new() -> Self { Self { base: BoxBase::new(), output_file: None, emit_obj_out: None, plugin_paths: None } } }
|
||||
|
||||
impl BoxCore for AotConfigBox {
|
||||
fn box_id(&self) -> u64 { self.base.id }
|
||||
@ -23,27 +24,30 @@ impl NyashBox for AotConfigBox {
|
||||
fn to_string_box(&self) -> StringBox { self.summary() }
|
||||
fn equals(&self, other: &dyn NyashBox) -> BoolBox { BoolBox::new(other.as_any().is::<AotConfigBox>()) }
|
||||
fn type_name(&self) -> &'static str { "AotConfigBox" }
|
||||
fn clone_box(&self) -> Box<dyn NyashBox> { Box::new(Self { base: self.base.clone(), output_file: self.output_file.clone(), emit_obj_out: self.emit_obj_out.clone() }) }
|
||||
fn clone_box(&self) -> Box<dyn NyashBox> { Box::new(Self { base: self.base.clone(), output_file: self.output_file.clone(), emit_obj_out: self.emit_obj_out.clone(), plugin_paths: self.plugin_paths.clone() }) }
|
||||
fn share_box(&self) -> Box<dyn NyashBox> { self.clone_box() }
|
||||
}
|
||||
|
||||
impl AotConfigBox {
|
||||
pub fn set_output(&mut self, path: &str) -> Box<dyn NyashBox> { self.output_file = Some(path.to_string()); Box::new(VoidBox::new()) }
|
||||
pub fn set_obj_out(&mut self, path: &str) -> Box<dyn NyashBox> { self.emit_obj_out = Some(path.to_string()); Box::new(VoidBox::new()) }
|
||||
pub fn clear(&mut self) -> Box<dyn NyashBox> { self.output_file = None; self.emit_obj_out = None; Box::new(VoidBox::new()) }
|
||||
pub fn set_plugin_paths(&mut self, paths: &str) -> Box<dyn NyashBox> { self.plugin_paths = Some(paths.to_string()); Box::new(VoidBox::new()) }
|
||||
pub fn clear(&mut self) -> Box<dyn NyashBox> { self.output_file = None; self.emit_obj_out = None; self.plugin_paths = None; Box::new(VoidBox::new()) }
|
||||
|
||||
/// Apply staged config to environment for CLI/runner consumption
|
||||
pub fn apply(&self) -> Box<dyn NyashBox> {
|
||||
if let Some(p) = &self.output_file { std::env::set_var("NYASH_AOT_OUT", p); }
|
||||
if let Some(p) = &self.emit_obj_out { std::env::set_var("NYASH_AOT_OBJECT_OUT", p); }
|
||||
if let Some(p) = &self.plugin_paths { std::env::set_var("NYASH_PLUGIN_PATHS", p); }
|
||||
Box::new(VoidBox::new())
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> StringBox {
|
||||
let s = format!(
|
||||
"output={} obj_out={}",
|
||||
"output={} obj_out={} plugin_paths={}",
|
||||
self.output_file.clone().unwrap_or_else(|| "<none>".to_string()),
|
||||
self.emit_obj_out.clone().unwrap_or_else(|| "<none>".to_string()),
|
||||
self.plugin_paths.clone().unwrap_or_else(|| "<none>".to_string()),
|
||||
);
|
||||
StringBox::new(s)
|
||||
}
|
||||
|
||||
@ -16,6 +16,14 @@ pub struct NyashConfigV2 {
|
||||
/// Plugin search paths
|
||||
#[serde(default)]
|
||||
pub plugin_paths: PluginPaths,
|
||||
|
||||
/// New: Plugins registry (name -> plugin root directory)
|
||||
#[serde(default)]
|
||||
pub plugins: HashMap<String, String>,
|
||||
|
||||
/// Optional central type_id mapping (box name -> type_id)
|
||||
#[serde(default)]
|
||||
pub box_types: HashMap<String, u32>,
|
||||
}
|
||||
|
||||
/// Library definition (simplified)
|
||||
@ -116,7 +124,23 @@ impl NyashConfigV2 {
|
||||
PluginPaths::default()
|
||||
};
|
||||
|
||||
Ok(NyashConfigV2 { libraries, plugin_paths })
|
||||
// Extract plugins map
|
||||
let plugins = if let Some(tbl) = config.get("plugins").and_then(|v| v.as_table()) {
|
||||
let mut m = HashMap::new();
|
||||
for (k, v) in tbl.iter() {
|
||||
if let Some(s) = v.as_str() { m.insert(k.clone(), s.to_string()); }
|
||||
}
|
||||
m
|
||||
} else { HashMap::new() };
|
||||
|
||||
// Extract optional box_types map
|
||||
let box_types = if let Some(tbl) = config.get("box_types").and_then(|v| v.as_table()) {
|
||||
let mut m = HashMap::new();
|
||||
for (k, v) in tbl.iter() { if let Some(id) = v.as_integer() { m.insert(k.clone(), id as u32); } }
|
||||
m
|
||||
} else { HashMap::new() };
|
||||
|
||||
Ok(NyashConfigV2 { libraries, plugin_paths, plugins, box_types })
|
||||
}
|
||||
|
||||
/// Parse library definitions with nested box configs
|
||||
@ -175,9 +199,15 @@ impl NyashConfigV2 {
|
||||
if std::path::Path::new(plugin_name).exists() {
|
||||
return Some(plugin_name.to_string());
|
||||
}
|
||||
|
||||
// Search in configured paths
|
||||
for search_path in &self.plugin_paths.search_paths {
|
||||
// Build effective search paths: config + ENV:NYASH_PLUGIN_PATHS (sep=';' or ':')
|
||||
let mut paths: Vec<String> = Vec::new();
|
||||
paths.extend(self.plugin_paths.search_paths.iter().cloned());
|
||||
if let Ok(envp) = std::env::var("NYASH_PLUGIN_PATHS") {
|
||||
let sep = if cfg!(target_os = "windows") { ';' } else { ':' };
|
||||
for p in envp.split(sep).filter(|s| !s.is_empty()) { paths.push(p.to_string()); }
|
||||
}
|
||||
// Search in effective paths
|
||||
for search_path in &paths {
|
||||
let path = std::path::Path::new(search_path).join(plugin_name);
|
||||
if path.exists() {
|
||||
return Some(path.to_string_lossy().to_string());
|
||||
|
||||
@ -260,15 +260,35 @@ impl PluginBoxV2 {
|
||||
}
|
||||
|
||||
/// Load all plugins from config
|
||||
pub fn load_all_plugins(&self) -> BidResult<()> {
|
||||
pub fn load_all_plugins(&self) -> BidResult<()> {
|
||||
let config = self.config.as_ref()
|
||||
.ok_or(BidError::PluginError)?;
|
||||
|
||||
// Load legacy libraries (backward compatible)
|
||||
for (lib_name, lib_def) in &config.libraries {
|
||||
if let Err(e) = self.load_plugin(lib_name, lib_def) {
|
||||
eprintln!("Warning: Failed to load plugin {}: {:?}", lib_name, e);
|
||||
}
|
||||
}
|
||||
// Load new-style plugins from [plugins] map (name -> root dir)
|
||||
for (plugin_name, root) in &config.plugins {
|
||||
// Synthesize a LibraryDefinition from plugin spec (nyash_box.toml) if present; otherwise minimal
|
||||
let mut boxes: Vec<String> = Vec::new();
|
||||
let spec_path = std::path::Path::new(root).join("nyash_box.toml");
|
||||
if let Ok(txt) = std::fs::read_to_string(&spec_path) {
|
||||
if let Ok(val) = txt.parse::<toml::Value>() {
|
||||
if let Some(prov) = val.get("provides").and_then(|t| t.get("boxes")).and_then(|a| a.as_array()) {
|
||||
for it in prov.iter() { if let Some(s) = it.as_str() { boxes.push(s.to_string()); } }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Path heuristic: use "<root>/<plugin_name>" (extension will be adapted by resolver)
|
||||
let synth_path = std::path::Path::new(root).join(plugin_name).to_string_lossy().to_string();
|
||||
let lib_def = LibraryDefinition { boxes: boxes.clone(), path: synth_path };
|
||||
if let Err(e) = self.load_plugin(plugin_name, &lib_def) {
|
||||
eprintln!("Warning: Failed to load plugin {} from [plugins]: {:?}", plugin_name, e);
|
||||
}
|
||||
}
|
||||
// Pre-birth singletons configured in nyash.toml
|
||||
let cfg_path = self.config_path.as_ref().map(|s| s.as_str()).unwrap_or("nyash.toml");
|
||||
let toml_content = std::fs::read_to_string(cfg_path).map_err(|_| BidError::PluginError)?;
|
||||
|
||||
@ -59,7 +59,7 @@ run_test() {
|
||||
# Run native executable
|
||||
echo -n " Native execution... "
|
||||
if ./app > /tmp/${test_name}_aot.out 2>&1; then
|
||||
AOT_RESULT=$(grep -oP 'ny_main\(\) returned: \K.*' /tmp/${test_name}_aot.out || echo "NO_RESULT")
|
||||
AOT_RESULT=$(grep -oP '^Result: \K.*' /tmp/${test_name}_aot.out || echo "NO_RESULT")
|
||||
echo "OK (Result: $AOT_RESULT)"
|
||||
else
|
||||
echo -e "${RED}FAILED${NC}"
|
||||
@ -108,4 +108,4 @@ if [[ $FAILED -eq 0 ]]; then
|
||||
else
|
||||
echo -e "\n${RED}Some tests failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user