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, Instrument};
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 `--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
108#[tracing::instrument(
109 name = "Building LuaJIT",
110 level = "info",
111 skip_all,
112 fields(version = LUAJIT_MM_VERSION),
113)]
114async fn do_build_luajit(args: BuildLua<'_>) -> Result<(), BuildLuaError> {
115 let build_dir = fs::tempfile::tempdir()?;
116 let luajit_url = "https://github.com/LuaJIT/LuaJIT.git";
119
120 {
121 let span = span!(
122 tracing::Level::INFO,
123 "Cloning LuaJIT sources",
124 url = luajit_url,
125 );
126 let _enter = span.enter(); let mut fetch_options = FetchOptions::new();
130 fetch_options.update_fetchhead(false);
131 let mut repo_builder = RepoBuilder::new();
132 repo_builder.fetch_options(fetch_options);
133 let repo = repo_builder.clone(luajit_url, build_dir.path())?;
134 let (object, _) = repo.revparse_ext(&format!("v{LUAJIT_MM_VERSION}"))?;
135 repo.checkout_tree(&object, None)?;
136 }
137 if cfg!(target_env = "msvc") {
138 do_build_luajit_msvc(args, build_dir.path()).await
139 } else {
140 do_build_luajit_unix(args, build_dir.path()).await
141 }
142}
143
144#[tracing::instrument(name = "Compiling LuaJIT", skip_all)]
145async fn do_build_luajit_unix(args: BuildLua<'_>, build_dir: &Path) -> Result<(), BuildLuaError> {
146 let lua_version = args.lua_version;
147 let config = args.config;
148 let install_dir = args.install_dir;
149
150 let host = Triple::host();
151
152 let mut cc = cc::Build::new();
153 cc.cargo_output(false)
154 .cargo_metadata(false)
155 .cargo_warnings(false)
156 .warnings(config.verbose())
157 .opt_level(3)
158 .host(&host.to_string())
159 .target(&host.to_string());
160 let compiler = cc.try_get_compiler()?;
161 let compiler_path = compiler.path().to_slash_lossy().to_string();
162 let mut make_cmd = Command::new(config.make_cmd());
163 make_cmd.current_dir(build_dir.join("src"));
164 make_cmd.arg("-e");
165 make_cmd.stdout(Stdio::piped());
166 make_cmd.stderr(Stdio::piped());
167 let target = host.to_string();
168 match target.as_str() {
169 "x86_64-apple-darwin" if env::var_os("MACOSX_DEPLOYMENT_TARGET").is_none() => {
170 make_cmd.env("MACOSX_DEPLOYMENT_TARGET", "10.11");
171 }
172 "aarch64-apple-darwin" if env::var_os("MACOSX_DEPLOYMENT_TARGET").is_none() => {
173 make_cmd.env("MACOSX_DEPLOYMENT_TARGET", "11.0");
174 }
175 _ if target.contains("linux") => {
176 make_cmd.env("TARGET_SYS", "Linux");
177 }
178 _ => {}
179 }
180 let compiler_path = which::which(&compiler_path)
181 .map_err(|err| io::Error::other(format!("cannot find {}:\n{}", compiler_path, err)))?;
182 let compiler_path = compiler_path.to_slash_lossy().to_string();
183 let compiler_args = compiler.cflags_env();
184 let compiler_args = compiler_args.to_string_lossy();
185 if env::var_os("STATIC_CC").is_none() {
186 make_cmd.env("STATIC_CC", format!("{compiler_path} {compiler_args}"));
187 }
188 if env::var_os("TARGET_LD").is_none() {
189 make_cmd.env("TARGET_LD", format!("{compiler_path} {compiler_args}"));
190 }
191 let mut xcflags = vec!["-fPIC"];
192 if lua_version == &LuaVersion::LuaJIT52 {
193 xcflags.push("-DLUAJIT_ENABLE_LUA52COMPAT");
194 }
195 if cfg!(debug_assertions) {
196 xcflags.push("-DLUA_USE_ASSERT");
197 xcflags.push("-DLUA_USE_APICHECK");
198 }
199 make_cmd.env("BUILDMODE", "static");
200 make_cmd.env("XCFLAGS", xcflags.join(" "));
201
202 match make_cmd.output().await {
203 Ok(output) if output.status.success() => utils::trace_command_output(&output),
204 Ok(output) => {
205 return Err(BuildLuaError::CommandFailure {
206 name: "build".into(),
207 status: output.status,
208 stdout: String::from_utf8_lossy(&output.stdout).into(),
209 stderr: String::from_utf8_lossy(&output.stderr).into(),
210 });
211 }
212 Err(err) => {
213 return Err(BuildLuaError::Io(io::Error::other(format!(
214 "Failed to run `{} build`:\n{}",
215 config.make_cmd(),
216 err
217 ))));
218 }
219 };
220
221 match Command::new(config.make_cmd())
222 .current_dir(build_dir)
223 .stdout(Stdio::piped())
224 .stderr(Stdio::piped())
225 .arg("install")
226 .arg(format!(r#"PREFIX="{}""#, install_dir.display()))
227 .output()
228 .await
229 {
230 Ok(output) if output.status.success() => utils::trace_command_output(&output),
231 Ok(output) => {
232 return Err(BuildLuaError::CommandFailure {
233 name: "install".into(),
234 status: output.status,
235 stdout: String::from_utf8_lossy(&output.stdout).into(),
236 stderr: String::from_utf8_lossy(&output.stderr).into(),
237 });
238 }
239 Err(err) => {
240 return Err(BuildLuaError::Io(io::Error::other(format!(
241 "Failed to run `{} install`:\n{}",
242 config.make_cmd(),
243 err
244 ))));
245 }
246 };
247 move_luajit_includes(install_dir).await?;
248 Ok(())
249}
250
251async fn move_luajit_includes(install_dir: &Path) -> io::Result<()> {
254 let include_dir = install_dir.join("include");
255 let include_subdir = include_dir.join(format!("luajit-{LUAJIT_MM_VERSION}"));
256 if !include_subdir.is_dir() {
257 return Ok(());
258 }
259 let mut dir = fs::tokio::read_dir(&include_subdir)
260 .await
261 .map_err(|err| io::Error::other(err.to_string()))?;
262 while let Some(entry) = dir.next_entry().await? {
263 let file_name = entry.file_name();
264 let src_path = entry.path();
265 let dest_path = include_dir.join(&file_name);
266 fs::tokio::copy(&src_path, &dest_path)
267 .await
268 .map_err(|err| io::Error::other(err.to_string()))?;
269 }
270 fs::tokio::remove_dir_all(&include_subdir)
271 .await
272 .map_err(|err| io::Error::other(err.to_string()))?;
273 Ok(())
274}
275
276#[tracing::instrument(name = "Compiling LuaJIT", skip_all)]
277async fn do_build_luajit_msvc(args: BuildLua<'_>, build_dir: &Path) -> Result<(), BuildLuaError> {
278 let lua_version = args.lua_version;
279 let install_dir = args.install_dir;
280 let lib_dir = install_dir.join("lib");
281 fs::tokio::create_dir_all(&lib_dir)
282 .await
283 .map_err(|err| io::Error::other(err.to_string()))?;
284 let include_dir = install_dir.join("include");
285 fs::tokio::create_dir_all(&include_dir)
286 .await
287 .map_err(|err| io::Error::other(err.to_string()))?;
288 let bin_dir = install_dir.join("bin");
289 fs::tokio::create_dir_all(&bin_dir)
290 .await
291 .map_err(|err| io::Error::other(err.to_string()))?;
292
293 let src_dir = build_dir.join("src");
294 let mut msvcbuild = Command::new(src_dir.join("msvcbuild.bat"));
295 msvcbuild.current_dir(&src_dir);
296 if lua_version == &LuaVersion::LuaJIT52 {
297 msvcbuild.arg("lua52compat");
298 }
299 msvcbuild.arg("static");
300 let host = Triple::host();
301 let target = host.to_string();
302 let cl = cc::windows_registry::find_tool(&target, "cl.exe").ok_or(BuildLuaError::ClNotFound)?;
303 for (k, v) in cl.env() {
304 msvcbuild.env(k, v);
305 }
306 fs::tokio::create_dir_all(&install_dir)
307 .await
308 .map_err(|err| io::Error::other(err.to_string()))?;
309 match msvcbuild.output().await {
310 Ok(output) if output.status.success() => utils::trace_command_output(&output),
311 Ok(output) => {
312 return Err(BuildLuaError::CommandFailure {
313 name: "build".into(),
314 status: output.status,
315 stdout: String::from_utf8_lossy(&output.stdout).into(),
316 stderr: String::from_utf8_lossy(&output.stderr).into(),
317 });
318 }
319 Err(err) => {
320 return Err(BuildLuaError::Io(io::Error::other(format!(
321 "Failed to run msvcbuild.bat:\n{}",
322 err
323 ))))
324 }
325 };
326
327 copy_includes(&src_dir, &include_dir).await?;
328 fs::tokio::copy(src_dir.join("lua51.lib"), lib_dir.join("luajit.lib"))
329 .await
330 .map_err(|err| io::Error::other(err.to_string()))?;
331 fs::tokio::copy(src_dir.join("luajit.exe"), bin_dir.join("luajit.exe"))
332 .await
333 .map_err(|err| io::Error::other(err.to_string()))?;
334 Ok(())
335}
336
337#[tracing::instrument(
338 name = "Building Lua",
339 level = "info",
340 skip_all,
341 fields(version = args.lua_version.to_string()),
342)]
343async fn do_build_lua(args: BuildLua<'_>) -> Result<(), BuildLuaError> {
344 let lua_version = args.lua_version;
345 let build_dir = fs::tempfile::tempdir()?;
346
347 let (source_integrity, pkg_version): (Integrity, &str) = unsafe {
348 match lua_version {
349 LuaVersion::Lua51 => (LUA51_HASH.parse().unwrap_unchecked(), LUA51_VERSION),
350 LuaVersion::Lua52 => (LUA52_HASH.parse().unwrap_unchecked(), LUA52_VERSION),
351 LuaVersion::Lua53 => (LUA53_HASH.parse().unwrap_unchecked(), LUA53_VERSION),
352 LuaVersion::Lua54 => (LUA54_HASH.parse().unwrap_unchecked(), LUA54_VERSION),
353 LuaVersion::Lua55 => (LUA55_HASH.parse().unwrap_unchecked(), LUA55_VERSION),
354 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => unreachable!(),
355 }
356 };
357
358 let file_name = format!("lua-{pkg_version}.tar.gz");
359
360 let source_url: Url = unsafe {
361 format!("https://www.lua.org/ftp/{file_name}")
362 .parse()
363 .unwrap_unchecked()
364 };
365
366 let response = crate::reqwest::https_client(args.config)?
367 .get(source_url.clone())
368 .send()
369 .instrument(tracing::info_span!("Downloading Lua"))
370 .await?
371 .error_for_status()?
372 .bytes()
373 .instrument(tracing::trace_span!("getting response body"))
374 .await?;
375
376 let hash = response.hash().await?;
377
378 if hash.matches(&source_integrity).is_none() {
379 return Err(BuildLuaError::SourceIntegrityMismatch {
380 src: source_url.to_string(),
381 expected: source_integrity,
382 actual: hash,
383 });
384 }
385
386 let cursor = Cursor::new(response);
387 let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
388 operations::unpack::unpack(mime_type, cursor, true, file_name, build_dir.path()).await?;
389
390 if cfg!(target_env = "msvc") {
391 do_build_lua_msvc(args, build_dir.path(), lua_version, pkg_version).await
392 } else {
393 do_build_lua_unix(args, build_dir.path(), lua_version, pkg_version).await
394 }
395}
396
397#[tracing::instrument(name = "Compiling Lua", skip_all)]
398async fn do_build_lua_unix(
399 args: BuildLua<'_>,
400 build_dir: &Path,
401 lua_version: &LuaVersion,
402 _pkg_version: &str,
403) -> Result<(), BuildLuaError> {
404 tracing::debug!(message = "Compiling Lua (Unix)...");
405 let config = args.config;
406 let install_dir = args.install_dir;
407
408 let build_target = if cfg!(target_os = "linux") {
409 if matches!(&lua_version, LuaVersion::Lua54) {
411 "linux-readline"
412 } else {
413 "linux"
414 }
415 } else if cfg!(target_os = "macos") {
416 "macosx"
417 } else if cfg!(target_os = "freebsd") {
418 "freebsd"
419 } else {
420 "generic"
421 };
422 match Command::new(config.make_cmd())
423 .current_dir(build_dir)
424 .stdout(Stdio::piped())
425 .stderr(Stdio::piped())
426 .arg(build_target)
427 .output()
428 .await
429 {
430 Ok(output) if !output.status.success() && build_target != "generic" => {
434 let fallback_output = Command::new(config.make_cmd())
435 .current_dir(build_dir)
436 .stdout(Stdio::piped())
437 .stderr(Stdio::piped())
438 .arg("generic")
439 .output()
440 .await;
441 guard_success(fallback_output, config, "build (generic)")?;
442 }
443 output => guard_success(output, config, &format!("build ({build_target})"))?,
444 };
445
446 match Command::new(config.make_cmd())
447 .current_dir(build_dir)
448 .stdout(Stdio::piped())
449 .stderr(Stdio::piped())
450 .arg("install")
451 .arg(format!(r#"INSTALL_TOP="{}""#, install_dir.display()))
452 .output()
453 .await
454 {
455 Ok(output) if output.status.success() => utils::trace_command_output(&output),
456 Ok(output) => {
457 return Err(BuildLuaError::CommandFailure {
458 name: "install".into(),
459 status: output.status,
460 stdout: String::from_utf8_lossy(&output.stdout).into(),
461 stderr: String::from_utf8_lossy(&output.stderr).into(),
462 });
463 }
464 Err(err) => {
465 return Err(BuildLuaError::Io(io::Error::other(format!(
466 "Failed to run `{} install`:\n{}",
467 config.make_cmd(),
468 err
469 ))))
470 }
471 };
472
473 Ok(())
474}
475
476#[tracing::instrument(name = "Compiling Lua", skip_all)]
477async fn do_build_lua_msvc(
478 args: BuildLua<'_>,
479 build_dir: &Path,
480 lua_version: &LuaVersion,
481 _pkg_version: &str,
482) -> Result<(), BuildLuaError> {
483 tracing::debug!(message = "Compiling Lua (MSVC)...");
484 let config = args.config;
485 let install_dir = args.install_dir;
486
487 let lib_dir = install_dir.join("lib");
488 fs::tokio::create_dir_all(&lib_dir)
489 .await
490 .map_err(|err| io::Error::other(err.to_string()))?;
491 let include_dir = install_dir.join("include");
492 fs::tokio::create_dir_all(&include_dir)
493 .await
494 .map_err(|err| io::Error::other(err.to_string()))?;
495 let bin_dir = install_dir.join("bin");
496 fs::tokio::create_dir_all(&bin_dir)
497 .await
498 .map_err(|err| io::Error::other(err.to_string()))?;
499
500 let src_dir = build_dir.join("src");
501
502 let lua_bin_name = "lua";
503 let lua_c_bin_name = "luac";
504
505 let dll_name = match lua_version {
506 LuaVersion::Lua51 => "lua51",
507 LuaVersion::Lua52 => "lua52",
508 LuaVersion::Lua53 => "lua53",
509 LuaVersion::Lua54 => "lua54",
510 LuaVersion::Lua55 => "lua55",
511 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => unreachable!(),
512 };
513
514 let lib_name = match lua_version {
515 LuaVersion::Lua51 => "lua5.1",
516 LuaVersion::Lua52 => "lua5.2",
517 LuaVersion::Lua53 => "lua5.3",
518 LuaVersion::Lua54 => "lua5.4",
519 LuaVersion::Lua55 => "lua5.5",
520 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => unreachable!(),
521 };
522
523 let host = Triple::host();
524 let mut cc = cc::Build::new();
525 cc.cargo_output(false)
526 .cargo_metadata(false)
527 .cargo_warnings(false)
528 .warnings(config.verbose())
529 .opt_level(3)
530 .host(&host.to_string())
531 .target(&host.to_string());
532
533 cc.define("LUA_USE_WINDOWS", None);
534 cc.define("LUA_BUILD_AS_DLL", None);
535
536 let mut lib_c_files = Vec::new();
537 let mut read_dir = fs::tokio::read_dir(&src_dir)
538 .await
539 .map_err(|err| io::Error::other(err.to_string()))?;
540 while let Some(entry) = read_dir.next_entry().await? {
541 let path = entry.path();
542 if path.extension().is_some_and(|ext| ext == "c")
543 && path
544 .with_extension("")
545 .file_name()
546 .is_some_and(|name| name != "lua" && name != "luac")
547 {
548 lib_c_files.push(path);
549 }
550 }
551
552 let lib_objects = cc
553 .include(&src_dir)
554 .files(lib_c_files)
555 .out_dir(&lib_dir)
556 .try_compile_objects(config)?;
557
558 let bin_objects = cc
559 .include(&src_dir)
560 .file(src_dir.join(format!("{lua_bin_name}.c")))
561 .file(src_dir.join(format!("{lua_c_bin_name}.c")))
562 .out_dir(&src_dir)
563 .try_compile_objects(config)?;
564
565 let lua_bin_objects = bin_objects.iter().filter(|file| {
566 file.file_stem().is_some_and(|fname| {
567 fname
568 .to_string_lossy()
569 .ends_with(&format!("-{lua_bin_name}"))
570 })
571 });
572
573 let lua_c_bin_objects = bin_objects.iter().filter(|file| {
574 file.file_stem().is_some_and(|fname| {
575 fname
576 .to_string_lossy()
577 .ends_with(&format!("-{lua_c_bin_name}"))
578 })
579 });
580
581 let target = host.to_string();
582 let link =
583 cc::windows_registry::find_tool(&target, "link.exe").ok_or(BuildLuaError::LinkNotFound)?;
584
585 let dll_path = bin_dir.join(format!("{dll_name}.dll"));
586 let lua_bin_path = bin_dir.join(format!("{lua_bin_name}.exe"));
587 let lua_c_bin_path = bin_dir.join(format!("{lua_c_bin_name}.exe"));
588
589 let implib_path = lib_dir.join(format!("{lib_name}.lib"));
590
591 guard_success(
593 Command::new(link.path())
594 .arg("/DLL")
595 .arg(format!("/OUT:{}", dll_path.display()))
596 .arg(format!("/IMPLIB:{}", implib_path.display()))
597 .args(&lib_objects)
598 .output()
599 .await,
600 config,
601 &format!("link {dll_name}.dll"),
602 )?;
603
604 guard_success(
606 Command::new(link.path())
607 .arg(format!("/OUT:{}", lua_bin_path.display()))
608 .arg(&implib_path)
609 .args(lua_bin_objects)
610 .output()
611 .await,
612 config,
613 &format!("link {}", lua_bin_path.display()),
614 )?;
615
616 guard_success(
618 Command::new(link.path())
619 .arg(format!("/OUT:{}", lua_c_bin_path.display()))
620 .args(&lib_objects)
621 .args(lua_c_bin_objects)
622 .output()
623 .await,
624 config,
625 &format!("link {}", lua_c_bin_path.display()),
626 )?;
627
628 copy_includes(&src_dir, &include_dir).await?;
629
630 Ok(())
631}
632
633fn guard_success(
634 output: io::Result<std::process::Output>,
635 config: &Config,
636 cmd_name: &str,
637) -> Result<(), BuildLuaError> {
638 match output {
639 Ok(output) if output.status.success() => {
640 utils::trace_command_output(&output);
641 Ok(())
642 }
643 Ok(output) => Err(BuildLuaError::CommandFailure {
644 name: cmd_name.to_string(),
645 status: output.status,
646 stdout: String::from_utf8_lossy(&output.stdout).into(),
647 stderr: String::from_utf8_lossy(&output.stderr).into(),
648 }),
649 Err(err) => Err(BuildLuaError::Io(io::Error::other(format!(
650 "Failed to run `{} build`:\n{}",
651 config.make_cmd(),
652 err
653 )))),
654 }
655}
656
657async fn copy_includes(src_dir: &Path, include_dir: &Path) -> Result<(), io::Error> {
658 for f in &[
659 "lauxlib.h",
660 "lua.h",
661 "luaconf.h",
662 "luajit.h",
663 "lualib.h",
664 "lua.hpp",
665 ] {
666 let src_file = src_dir.join(f);
667 if src_file.is_file() {
668 fs::tokio::copy(&src_file, include_dir.join(f))
669 .await
670 .map_err(|err| io::Error::other(err.to_string()))?;
671 }
672 }
673 Ok(())
674}