1use std::{
2 env,
3 io::{self, Cursor},
4 path::Path,
5 process::{ExitStatus, Stdio},
6};
7
8use crate::{
9 build::utils::{self, CCExt},
10 config::Config,
11 fs,
12 hash::HasIntegrity,
13 lua_version::LuaVersion,
14 operations::{self, UnpackError},
15};
16use bon::Builder;
17use git2::{build::RepoBuilder, FetchOptions};
18use miette::Diagnostic;
19use path_slash::PathExt;
20use ssri::Integrity;
21use target_lexicon::Triple;
22use thiserror::Error;
23use tokio::process::Command;
24use tracing::span;
25use url::Url;
26
27const LUA51_VERSION: &str = "5.1.5";
28const LUA51_HASH: &str = "sha256-JkD8VqeV8p0o7xXhPDSkfiI5YLAkDoywqC2bBzhpUzM=";
29const LUA52_VERSION: &str = "5.2.4";
30const LUA52_HASH: &str = "sha256-ueLkqtZ4mztjoFbUQveznw7Pyjrg8fwK5OlhRAG2n0s=";
31const LUA53_VERSION: &str = "5.3.6";
32const LUA53_HASH: &str = "sha256-/F/Wm7hzYyPwJmcrG3I12mE9cXfnJViJOgvc0yBGbWA=";
33const LUA54_VERSION: &str = "5.4.8";
34const LUA54_HASH: &str = "sha256-TxjdrhVOeT5G7qtyfFnvHAwMK3ROe5QhlxDXb1MGKa4=";
35const LUA55_VERSION: &str = "5.5.0";
36const LUA55_HASH: &str = "sha256-V8zDK7vQBcq3W8xSREBSU1r2kXiduiuQFtXFBkDWiz0=";
37const LUAJIT_MM_VERSION: &str = "2.1";
40
41#[derive(Builder)]
42#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
43pub struct BuildLua<'a> {
44 lua_version: &'a LuaVersion,
45 install_dir: &'a Path,
46 config: &'a Config,
47}
48
49#[derive(Debug, Error, Diagnostic)]
50pub enum BuildLuaError {
51 #[error(transparent)]
52 Request(#[from] reqwest::Error),
53 #[error(transparent)]
54 Io(#[from] io::Error),
55 #[error(transparent)]
56 #[diagnostic(transparent)]
57 Fs(#[from] fs::FsError),
58 #[error(transparent)]
59 #[diagnostic(transparent)]
60 Unpack(#[from] UnpackError),
61 #[error(transparent)]
62 Git(#[from] git2::Error),
63 #[error(transparent)]
64 CC(#[from] cc::Error),
65 #[error("failed to find cl.exe")]
66 ClNotFound,
67 #[error("failed to find LINK.exe")]
68 LinkNotFound,
69 #[error(
70 r#"source integrity mismatch.
71- source: {src}
72- expected: {expected}
73- got: {actual}"#
74 )]
75 #[diagnostic(help(
76 r#"the source may have been modified or a tag may have been moved.
77check the source, then rerun the command with `lx --no-lock` to update the hash."#
78 ))]
79 SourceIntegrityMismatch {
80 src: String,
81 expected: Integrity,
82 actual: Integrity,
83 },
84 #[error("{name} failed.\n\n{status}\n\nstdout:\n{stdout}\n\nstderr:\n{stderr}")]
85 CommandFailure {
86 name: String,
87 status: ExitStatus,
88 stdout: String,
89 stderr: String,
90 },
91}
92
93impl<State: build_lua_builder::State + build_lua_builder::IsComplete> BuildLuaBuilder<'_, State> {
94 pub async fn build(self) -> Result<(), BuildLuaError> {
95 let args = self._build();
96 let lua_version = args.lua_version;
97 match lua_version {
98 LuaVersion::Lua51
99 | LuaVersion::Lua52
100 | LuaVersion::Lua53
101 | LuaVersion::Lua54
102 | LuaVersion::Lua55 => do_build_lua(args).await,
103 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => do_build_luajit(args).await,
104 }
105 }
106}
107
108async fn do_build_luajit(args: BuildLua<'_>) -> Result<(), BuildLuaError> {
109 let span = span!(
110 tracing::Level::INFO,
111 "Building LuaJIT",
112 version = LUAJIT_MM_VERSION,
113 );
114 let _enter = span.enter();
115
116 let build_dir = fs::tempfile::tempdir()?;
117 let luajit_url = "https://github.com/LuaJIT/LuaJIT.git";
120
121 {
122 let span = span!(
123 tracing::Level::INFO,
124 "Cloning LuaJIT sources",
125 url = luajit_url,
126 );
127 let _enter = span.enter();
128
129 let mut fetch_options = FetchOptions::new();
131 fetch_options.update_fetchhead(false);
132 let mut repo_builder = RepoBuilder::new();
133 repo_builder.fetch_options(fetch_options);
134 let repo = repo_builder.clone(luajit_url, build_dir.path())?;
135 let (object, _) = repo.revparse_ext(&format!("v{LUAJIT_MM_VERSION}"))?;
136 repo.checkout_tree(&object, None)?;
137 }
138 if cfg!(target_env = "msvc") {
139 do_build_luajit_msvc(args, build_dir.path()).await
140 } else {
141 do_build_luajit_unix(args, build_dir.path()).await
142 }
143}
144
145#[tracing::instrument(name = "Compiling LuaJIT", skip_all)]
146async fn do_build_luajit_unix(args: BuildLua<'_>, build_dir: &Path) -> Result<(), BuildLuaError> {
147 let lua_version = args.lua_version;
148 let config = args.config;
149 let install_dir = args.install_dir;
150
151 let host = Triple::host();
152
153 let mut cc = cc::Build::new();
154 cc.cargo_output(false)
155 .cargo_metadata(false)
156 .cargo_warnings(false)
157 .warnings(config.verbose())
158 .opt_level(3)
159 .host(&host.to_string())
160 .target(&host.to_string());
161 let compiler = cc.try_get_compiler()?;
162 let compiler_path = compiler.path().to_slash_lossy().to_string();
163 let mut make_cmd = Command::new(config.make_cmd());
164 make_cmd.current_dir(build_dir.join("src"));
165 make_cmd.arg("-e");
166 make_cmd.stdout(Stdio::piped());
167 make_cmd.stderr(Stdio::piped());
168 let target = host.to_string();
169 match target.as_str() {
170 "x86_64-apple-darwin" if env::var_os("MACOSX_DEPLOYMENT_TARGET").is_none() => {
171 make_cmd.env("MACOSX_DEPLOYMENT_TARGET", "10.11");
172 }
173 "aarch64-apple-darwin" if env::var_os("MACOSX_DEPLOYMENT_TARGET").is_none() => {
174 make_cmd.env("MACOSX_DEPLOYMENT_TARGET", "11.0");
175 }
176 _ if target.contains("linux") => {
177 make_cmd.env("TARGET_SYS", "Linux");
178 }
179 _ => {}
180 }
181 let compiler_path = which::which(&compiler_path)
182 .map_err(|err| io::Error::other(format!("cannot find {}:\n{}", compiler_path, err)))?;
183 let compiler_path = compiler_path.to_slash_lossy().to_string();
184 let compiler_args = compiler.cflags_env();
185 let compiler_args = compiler_args.to_string_lossy();
186 if env::var_os("STATIC_CC").is_none() {
187 make_cmd.env("STATIC_CC", format!("{compiler_path} {compiler_args}"));
188 }
189 if env::var_os("TARGET_LD").is_none() {
190 make_cmd.env("TARGET_LD", format!("{compiler_path} {compiler_args}"));
191 }
192 let mut xcflags = vec!["-fPIC"];
193 if lua_version == &LuaVersion::LuaJIT52 {
194 xcflags.push("-DLUAJIT_ENABLE_LUA52COMPAT");
195 }
196 if cfg!(debug_assertions) {
197 xcflags.push("-DLUA_USE_ASSERT");
198 xcflags.push("-DLUA_USE_APICHECK");
199 }
200 make_cmd.env("BUILDMODE", "static");
201 make_cmd.env("XCFLAGS", xcflags.join(" "));
202
203 match make_cmd.output().await {
204 Ok(output) if output.status.success() => utils::trace_command_output(&output),
205 Ok(output) => {
206 return Err(BuildLuaError::CommandFailure {
207 name: "build".into(),
208 status: output.status,
209 stdout: String::from_utf8_lossy(&output.stdout).into(),
210 stderr: String::from_utf8_lossy(&output.stderr).into(),
211 });
212 }
213 Err(err) => {
214 return Err(BuildLuaError::Io(io::Error::other(format!(
215 "Failed to run `{} build`:\n{}",
216 config.make_cmd(),
217 err
218 ))));
219 }
220 };
221
222 match Command::new(config.make_cmd())
223 .current_dir(build_dir)
224 .stdout(Stdio::piped())
225 .stderr(Stdio::piped())
226 .arg("install")
227 .arg(format!(r#"PREFIX="{}""#, install_dir.display()))
228 .output()
229 .await
230 {
231 Ok(output) if output.status.success() => utils::trace_command_output(&output),
232 Ok(output) => {
233 return Err(BuildLuaError::CommandFailure {
234 name: "install".into(),
235 status: output.status,
236 stdout: String::from_utf8_lossy(&output.stdout).into(),
237 stderr: String::from_utf8_lossy(&output.stderr).into(),
238 });
239 }
240 Err(err) => {
241 return Err(BuildLuaError::Io(io::Error::other(format!(
242 "Failed to run `{} install`:\n{}",
243 config.make_cmd(),
244 err
245 ))));
246 }
247 };
248 move_luajit_includes(install_dir).await?;
249 Ok(())
250}
251
252async fn move_luajit_includes(install_dir: &Path) -> io::Result<()> {
255 let include_dir = install_dir.join("include");
256 let include_subdir = include_dir.join(format!("luajit-{LUAJIT_MM_VERSION}"));
257 if !include_subdir.is_dir() {
258 return Ok(());
259 }
260 let mut dir = fs::tokio::read_dir(&include_subdir)
261 .await
262 .map_err(|err| io::Error::other(err.to_string()))?;
263 while let Some(entry) = dir.next_entry().await? {
264 let file_name = entry.file_name();
265 let src_path = entry.path();
266 let dest_path = include_dir.join(&file_name);
267 fs::tokio::copy(&src_path, &dest_path)
268 .await
269 .map_err(|err| io::Error::other(err.to_string()))?;
270 }
271 fs::tokio::remove_dir_all(&include_subdir)
272 .await
273 .map_err(|err| io::Error::other(err.to_string()))?;
274 Ok(())
275}
276
277#[tracing::instrument(name = "Compiling LuaJIT", skip_all)]
278async fn do_build_luajit_msvc(args: BuildLua<'_>, build_dir: &Path) -> Result<(), BuildLuaError> {
279 let lua_version = args.lua_version;
280 let install_dir = args.install_dir;
281 let lib_dir = install_dir.join("lib");
282 fs::tokio::create_dir_all(&lib_dir)
283 .await
284 .map_err(|err| io::Error::other(err.to_string()))?;
285 let include_dir = install_dir.join("include");
286 fs::tokio::create_dir_all(&include_dir)
287 .await
288 .map_err(|err| io::Error::other(err.to_string()))?;
289 let bin_dir = install_dir.join("bin");
290 fs::tokio::create_dir_all(&bin_dir)
291 .await
292 .map_err(|err| io::Error::other(err.to_string()))?;
293
294 let src_dir = build_dir.join("src");
295 let mut msvcbuild = Command::new(src_dir.join("msvcbuild.bat"));
296 msvcbuild.current_dir(&src_dir);
297 if lua_version == &LuaVersion::LuaJIT52 {
298 msvcbuild.arg("lua52compat");
299 }
300 msvcbuild.arg("static");
301 let host = Triple::host();
302 let target = host.to_string();
303 let cl = cc::windows_registry::find_tool(&target, "cl.exe").ok_or(BuildLuaError::ClNotFound)?;
304 for (k, v) in cl.env() {
305 msvcbuild.env(k, v);
306 }
307 fs::tokio::create_dir_all(&install_dir)
308 .await
309 .map_err(|err| io::Error::other(err.to_string()))?;
310 match msvcbuild.output().await {
311 Ok(output) if output.status.success() => utils::trace_command_output(&output),
312 Ok(output) => {
313 return Err(BuildLuaError::CommandFailure {
314 name: "build".into(),
315 status: output.status,
316 stdout: String::from_utf8_lossy(&output.stdout).into(),
317 stderr: String::from_utf8_lossy(&output.stderr).into(),
318 });
319 }
320 Err(err) => {
321 return Err(BuildLuaError::Io(io::Error::other(format!(
322 "Failed to run msvcbuild.bat:\n{}",
323 err
324 ))))
325 }
326 };
327
328 copy_includes(&src_dir, &include_dir).await?;
329 fs::tokio::copy(src_dir.join("lua51.lib"), lib_dir.join("luajit.lib"))
330 .await
331 .map_err(|err| io::Error::other(err.to_string()))?;
332 fs::tokio::copy(src_dir.join("luajit.exe"), bin_dir.join("luajit.exe"))
333 .await
334 .map_err(|err| io::Error::other(err.to_string()))?;
335 Ok(())
336}
337
338async fn do_build_lua(args: BuildLua<'_>) -> Result<(), BuildLuaError> {
339 let lua_version = args.lua_version;
340 let span = span!(
341 tracing::Level::INFO,
342 "Building Lua",
343 version = lua_version.to_string(),
344 );
345 let _enter = span.enter();
346
347 let build_dir = fs::tempfile::tempdir()?;
348
349 let (source_integrity, pkg_version): (Integrity, &str) = unsafe {
350 match lua_version {
351 LuaVersion::Lua51 => (LUA51_HASH.parse().unwrap_unchecked(), LUA51_VERSION),
352 LuaVersion::Lua52 => (LUA52_HASH.parse().unwrap_unchecked(), LUA52_VERSION),
353 LuaVersion::Lua53 => (LUA53_HASH.parse().unwrap_unchecked(), LUA53_VERSION),
354 LuaVersion::Lua54 => (LUA54_HASH.parse().unwrap_unchecked(), LUA54_VERSION),
355 LuaVersion::Lua55 => (LUA55_HASH.parse().unwrap_unchecked(), LUA55_VERSION),
356 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => unreachable!(),
357 }
358 };
359
360 let file_name = format!("lua-{pkg_version}.tar.gz");
361
362 let source_url: Url = unsafe {
363 format!("https://www.lua.org/ftp/{file_name}")
364 .parse()
365 .unwrap_unchecked()
366 };
367
368 let response = {
369 let span = span!(
370 tracing::Level::INFO,
371 "Downloading Lua",
372 url = source_url.to_string(),
373 );
374 let _enter = span.enter();
375
376 crate::reqwest::new_https_client(args.config)?
377 .get(source_url.clone())
378 .send()
379 .await?
380 .error_for_status()?
381 .bytes()
382 .await?
383 };
384
385 let hash = response.hash()?;
386
387 if hash.matches(&source_integrity).is_none() {
388 return Err(BuildLuaError::SourceIntegrityMismatch {
389 src: source_url.to_string(),
390 expected: source_integrity,
391 actual: hash,
392 });
393 }
394
395 let cursor = Cursor::new(response);
396 let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
397 operations::unpack::unpack(mime_type, cursor, true, file_name, build_dir.path()).await?;
398
399 if cfg!(target_env = "msvc") {
400 do_build_lua_msvc(args, build_dir.path(), lua_version, pkg_version).await
401 } else {
402 do_build_lua_unix(args, build_dir.path(), lua_version, pkg_version).await
403 }
404}
405
406#[tracing::instrument(name = "Compiling Lua", skip_all)]
407async fn do_build_lua_unix(
408 args: BuildLua<'_>,
409 build_dir: &Path,
410 lua_version: &LuaVersion,
411 _pkg_version: &str,
412) -> Result<(), BuildLuaError> {
413 tracing::debug!(message = "Compiling Lua (Unix)...");
414 let config = args.config;
415 let install_dir = args.install_dir;
416
417 let build_target = if cfg!(target_os = "linux") {
418 if matches!(&lua_version, LuaVersion::Lua54) {
420 "linux-readline"
421 } else {
422 "linux"
423 }
424 } else if cfg!(target_os = "macos") {
425 "macosx"
426 } else if cfg!(target_os = "freebsd") {
427 "freebsd"
428 } else {
429 "generic"
430 };
431 match Command::new(config.make_cmd())
432 .current_dir(build_dir)
433 .stdout(Stdio::piped())
434 .stderr(Stdio::piped())
435 .arg(build_target)
436 .output()
437 .await
438 {
439 Ok(output) if !output.status.success() && build_target != "generic" => {
443 let fallback_output = Command::new(config.make_cmd())
444 .current_dir(build_dir)
445 .stdout(Stdio::piped())
446 .stderr(Stdio::piped())
447 .arg("generic")
448 .output()
449 .await;
450 guard_success(fallback_output, config, "build (generic)")?;
451 }
452 output => guard_success(output, config, &format!("build ({build_target})"))?,
453 };
454
455 match Command::new(config.make_cmd())
456 .current_dir(build_dir)
457 .stdout(Stdio::piped())
458 .stderr(Stdio::piped())
459 .arg("install")
460 .arg(format!(r#"INSTALL_TOP="{}""#, install_dir.display()))
461 .output()
462 .await
463 {
464 Ok(output) if output.status.success() => utils::trace_command_output(&output),
465 Ok(output) => {
466 return Err(BuildLuaError::CommandFailure {
467 name: "install".into(),
468 status: output.status,
469 stdout: String::from_utf8_lossy(&output.stdout).into(),
470 stderr: String::from_utf8_lossy(&output.stderr).into(),
471 });
472 }
473 Err(err) => {
474 return Err(BuildLuaError::Io(io::Error::other(format!(
475 "Failed to run `{} install`:\n{}",
476 config.make_cmd(),
477 err
478 ))))
479 }
480 };
481
482 Ok(())
483}
484
485#[tracing::instrument(name = "Compiling Lua", skip_all)]
486async fn do_build_lua_msvc(
487 args: BuildLua<'_>,
488 build_dir: &Path,
489 lua_version: &LuaVersion,
490 _pkg_version: &str,
491) -> Result<(), BuildLuaError> {
492 tracing::debug!(message = "Compiling Lua (MSVC)...");
493 let config = args.config;
494 let install_dir = args.install_dir;
495
496 let lib_dir = install_dir.join("lib");
497 fs::tokio::create_dir_all(&lib_dir)
498 .await
499 .map_err(|err| io::Error::other(err.to_string()))?;
500 let include_dir = install_dir.join("include");
501 fs::tokio::create_dir_all(&include_dir)
502 .await
503 .map_err(|err| io::Error::other(err.to_string()))?;
504 let bin_dir = install_dir.join("bin");
505 fs::tokio::create_dir_all(&bin_dir)
506 .await
507 .map_err(|err| io::Error::other(err.to_string()))?;
508
509 let src_dir = build_dir.join("src");
510
511 let lua_bin_name = "lua";
512 let lua_c_bin_name = "luac";
513
514 let dll_name = match lua_version {
515 LuaVersion::Lua51 => "lua51",
516 LuaVersion::Lua52 => "lua52",
517 LuaVersion::Lua53 => "lua53",
518 LuaVersion::Lua54 => "lua54",
519 LuaVersion::Lua55 => "lua55",
520 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => unreachable!(),
521 };
522
523 let lib_name = match lua_version {
524 LuaVersion::Lua51 => "lua5.1",
525 LuaVersion::Lua52 => "lua5.2",
526 LuaVersion::Lua53 => "lua5.3",
527 LuaVersion::Lua54 => "lua5.4",
528 LuaVersion::Lua55 => "lua5.5",
529 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => unreachable!(),
530 };
531
532 let host = Triple::host();
533 let mut cc = cc::Build::new();
534 cc.cargo_output(false)
535 .cargo_metadata(false)
536 .cargo_warnings(false)
537 .warnings(config.verbose())
538 .opt_level(3)
539 .host(&host.to_string())
540 .target(&host.to_string());
541
542 cc.define("LUA_USE_WINDOWS", None);
543 cc.define("LUA_BUILD_AS_DLL", None);
544
545 let mut lib_c_files = Vec::new();
546 let mut read_dir = fs::tokio::read_dir(&src_dir)
547 .await
548 .map_err(|err| io::Error::other(err.to_string()))?;
549 while let Some(entry) = read_dir.next_entry().await? {
550 let path = entry.path();
551 if path.extension().is_some_and(|ext| ext == "c")
552 && path
553 .with_extension("")
554 .file_name()
555 .is_some_and(|name| name != "lua" && name != "luac")
556 {
557 lib_c_files.push(path);
558 }
559 }
560
561 let lib_objects = cc
562 .include(&src_dir)
563 .files(lib_c_files)
564 .out_dir(&lib_dir)
565 .try_compile_objects(config)?;
566
567 let bin_objects = cc
568 .include(&src_dir)
569 .file(src_dir.join(format!("{lua_bin_name}.c")))
570 .file(src_dir.join(format!("{lua_c_bin_name}.c")))
571 .out_dir(&src_dir)
572 .try_compile_objects(config)?;
573
574 let lua_bin_objects = bin_objects.iter().filter(|file| {
575 file.file_stem().is_some_and(|fname| {
576 fname
577 .to_string_lossy()
578 .ends_with(&format!("-{lua_bin_name}"))
579 })
580 });
581
582 let lua_c_bin_objects = bin_objects.iter().filter(|file| {
583 file.file_stem().is_some_and(|fname| {
584 fname
585 .to_string_lossy()
586 .ends_with(&format!("-{lua_c_bin_name}"))
587 })
588 });
589
590 let target = host.to_string();
591 let link =
592 cc::windows_registry::find_tool(&target, "link.exe").ok_or(BuildLuaError::LinkNotFound)?;
593
594 let dll_path = bin_dir.join(format!("{dll_name}.dll"));
595 let lua_bin_path = bin_dir.join(format!("{lua_bin_name}.exe"));
596 let lua_c_bin_path = bin_dir.join(format!("{lua_c_bin_name}.exe"));
597
598 let implib_path = lib_dir.join(format!("{lib_name}.lib"));
599
600 guard_success(
602 Command::new(link.path())
603 .arg("/DLL")
604 .arg(format!("/OUT:{}", dll_path.display()))
605 .arg(format!("/IMPLIB:{}", implib_path.display()))
606 .args(&lib_objects)
607 .output()
608 .await,
609 config,
610 &format!("link {dll_name}.dll"),
611 )?;
612
613 guard_success(
615 Command::new(link.path())
616 .arg(format!("/OUT:{}", lua_bin_path.display()))
617 .arg(&implib_path)
618 .args(lua_bin_objects)
619 .output()
620 .await,
621 config,
622 &format!("link {}", lua_bin_path.display()),
623 )?;
624
625 guard_success(
627 Command::new(link.path())
628 .arg(format!("/OUT:{}", lua_c_bin_path.display()))
629 .args(&lib_objects)
630 .args(lua_c_bin_objects)
631 .output()
632 .await,
633 config,
634 &format!("link {}", lua_c_bin_path.display()),
635 )?;
636
637 copy_includes(&src_dir, &include_dir).await?;
638
639 Ok(())
640}
641
642fn guard_success(
643 output: io::Result<std::process::Output>,
644 config: &Config,
645 cmd_name: &str,
646) -> Result<(), BuildLuaError> {
647 match output {
648 Ok(output) if output.status.success() => {
649 utils::trace_command_output(&output);
650 Ok(())
651 }
652 Ok(output) => Err(BuildLuaError::CommandFailure {
653 name: cmd_name.to_string(),
654 status: output.status,
655 stdout: String::from_utf8_lossy(&output.stdout).into(),
656 stderr: String::from_utf8_lossy(&output.stderr).into(),
657 }),
658 Err(err) => Err(BuildLuaError::Io(io::Error::other(format!(
659 "Failed to run `{} build`:\n{}",
660 config.make_cmd(),
661 err
662 )))),
663 }
664}
665
666async fn copy_includes(src_dir: &Path, include_dir: &Path) -> Result<(), io::Error> {
667 for f in &[
668 "lauxlib.h",
669 "lua.h",
670 "luaconf.h",
671 "luajit.h",
672 "lualib.h",
673 "lua.hpp",
674 ] {
675 let src_file = src_dir.join(f);
676 if src_file.is_file() {
677 fs::tokio::copy(&src_file, include_dir.join(f))
678 .await
679 .map_err(|err| io::Error::other(err.to_string()))?;
680 }
681 }
682 Ok(())
683}