Fork of daniellemaywood.uk/gleam — Wasm codegen work
27 kB
893 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2020 The Gleam contributors
3
4use gleam_core::{
5 Result, Warning,
6 build::{NullTelemetry, Target},
7 error::{Error, FileIoAction, FileKind, OS, ShellCommandFailureReason, parse_os},
8 io::{
9 BeamCompilerIO, Command, CommandExecutor, Content, DirEntry, FileSystemReader,
10 FileSystemWriter, OutputFile, ReadDir, Stdio, WrappedReader, is_native_file_extension,
11 },
12 manifest::Manifest,
13 paths::ProjectPaths,
14 warning::WarningEmitterIO,
15};
16use gleam_language_server::{DownloadDependencies, Locker, MakeLocker};
17use std::{
18 collections::HashSet,
19 fmt::Debug,
20 fs::{File, exists},
21 io::{self, BufRead, BufReader, Write},
22 sync::{Arc, Mutex, OnceLock},
23 time::SystemTime,
24};
25
26use camino::{ReadDirUtf8, Utf8Path, Utf8PathBuf};
27
28use crate::{beam_compiler::BeamCompilerInstance, dependencies, lsp::LspLocker};
29
30#[cfg(test)]
31mod tests;
32
33/// Return the current directory as a UTF-8 Path
34pub fn get_current_directory() -> Result<Utf8PathBuf, Error> {
35 let curr_dir = std::env::current_dir().map_err(|e| Error::FileIo {
36 kind: FileKind::Directory,
37 action: FileIoAction::Open,
38 path: ".".into(),
39 err: Some(e.to_string()),
40 })?;
41 Utf8PathBuf::from_path_buf(curr_dir.clone()).map_err(|_| Error::NonUtf8Path { path: curr_dir })
42}
43
44// Return the first directory with a gleam.toml as a UTF-8 Path
45pub fn get_project_root(original_path: Utf8PathBuf) -> Result<Utf8PathBuf, Error> {
46 let mut path = original_path.clone();
47 loop {
48 if path.join("gleam.toml").is_file() {
49 return Ok(path);
50 }
51
52 path = match path.parent() {
53 Some(path) => path.into(),
54 None => {
55 return Err(Error::UnableToFindProjectRoot {
56 path: original_path.to_string(),
57 });
58 }
59 }
60 }
61}
62
63pub fn get_os() -> OS {
64 parse_os(std::env::consts::OS, get_distro_str().as_str())
65}
66
67// try to extract the distro id from /etc/os-release
68pub fn extract_distro_id(os_release: String) -> String {
69 let distro = os_release.lines().find(|line| line.starts_with("ID="));
70 if let Some(distro) = distro {
71 let id = distro.split('=').nth(1).unwrap_or("").replace("\"", "");
72 return id;
73 }
74 "".to_string()
75}
76
77pub fn get_distro_str() -> String {
78 let path = Utf8Path::new("/etc/os-release");
79 if std::env::consts::OS != "linux" || !path.exists() {
80 return "other".to_string();
81 }
82 let os_release = read(path);
83 match os_release {
84 Ok(os_release) => extract_distro_id(os_release),
85 Err(_) => "other".to_string(),
86 }
87}
88
89/// A `FileWriter` implementation that writes to the file system.
90#[derive(Debug, Clone, Default)]
91pub struct ProjectIO {
92 beam_compiler: Arc<Mutex<Option<BeamCompilerInstance>>>,
93}
94
95impl ProjectIO {
96 pub fn new() -> Self {
97 Self {
98 beam_compiler: Default::default(),
99 }
100 }
101
102 pub fn boxed() -> Box<Self> {
103 Box::new(Self::new())
104 }
105
106 pub(crate) fn initialise_beam_compiler(&self) -> Result<(), Error> {
107 let mut guard = self
108 .beam_compiler
109 .lock()
110 .expect("could not lock beam_compiler");
111 *guard = Some(BeamCompilerInstance::new(self)?);
112 Ok(())
113 }
114}
115
116impl FileSystemReader for ProjectIO {
117 fn read(&self, path: &Utf8Path) -> Result<String, Error> {
118 read(path)
119 }
120
121 fn read_bytes(&self, path: &Utf8Path) -> Result<Vec<u8>, Error> {
122 read_bytes(path)
123 }
124
125 fn is_file(&self, path: &Utf8Path) -> bool {
126 path.is_file()
127 }
128
129 fn is_directory(&self, path: &Utf8Path) -> bool {
130 path.is_dir()
131 }
132
133 fn reader(&self, path: &Utf8Path) -> Result<WrappedReader, Error> {
134 reader(path)
135 }
136
137 fn read_dir(&self, path: &Utf8Path) -> Result<ReadDir> {
138 read_dir(path).map(|entries| {
139 entries
140 .map(|result| result.map(|entry| DirEntry::from_path(entry.path())))
141 .collect()
142 })
143 }
144
145 fn modification_time(&self, path: &Utf8Path) -> Result<SystemTime, Error> {
146 modification_time(path)
147 }
148
149 fn canonicalise(&self, path: &Utf8Path) -> Result<Utf8PathBuf, Error> {
150 canonicalise(path)
151 }
152
153 fn is_same_file(&self, left: &Utf8Path, right: &Utf8Path) -> Result<bool, Error> {
154 is_same_file(left, right)
155 }
156}
157
158fn is_same_file(left: &Utf8Path, right: &Utf8Path) -> Result<bool, Error> {
159 same_file::is_same_file(left, right).map_err(|e| Error::FileIo {
160 action: FileIoAction::ReadMetadata,
161 kind: FileKind::File,
162 path: left.to_path_buf(),
163 err: Some(e.to_string()),
164 })
165}
166
167pub fn modification_time(path: &Utf8Path) -> std::result::Result<SystemTime, Error> {
168 path.metadata()
169 .map(|m| m.modified().unwrap_or_else(|_| SystemTime::now()))
170 .map_err(|e| Error::FileIo {
171 action: FileIoAction::ReadMetadata,
172 kind: FileKind::File,
173 path: path.to_path_buf(),
174 err: Some(e.to_string()),
175 })
176}
177
178impl FileSystemWriter for ProjectIO {
179 fn delete_directory(&self, path: &Utf8Path) -> Result<()> {
180 delete_directory(path)
181 }
182
183 fn copy(&self, from: &Utf8Path, to: &Utf8Path) -> Result<()> {
184 copy(from, to)
185 }
186
187 fn copy_dir(&self, from: &Utf8Path, to: &Utf8Path) -> Result<()> {
188 copy_dir(from, to)
189 }
190
191 fn mkdir(&self, path: &Utf8Path) -> Result<(), Error> {
192 mkdir(path)
193 }
194
195 fn hardlink(&self, from: &Utf8Path, to: &Utf8Path) -> Result<(), Error> {
196 hardlink(from, to)
197 }
198
199 fn symlink_dir(&self, from: &Utf8Path, to: &Utf8Path) -> Result<(), Error> {
200 symlink_dir(from, to)
201 }
202
203 fn delete_file(&self, path: &Utf8Path) -> Result<()> {
204 delete_file(path)
205 }
206
207 fn write(&self, path: &Utf8Path, content: &str) -> Result<(), Error> {
208 write(path, content)
209 }
210
211 fn write_bytes(&self, path: &Utf8Path, content: &[u8]) -> Result<(), Error> {
212 write_bytes(path, content)
213 }
214
215 fn exists(&self, path: &Utf8Path) -> bool {
216 path.exists()
217 }
218}
219
220impl CommandExecutor for ProjectIO {
221 fn exec(&self, command: Command) -> Result<i32, Error> {
222 let Command {
223 program,
224 args,
225 env,
226 cwd,
227 stdio,
228 } = command;
229 tracing::debug!(program=program, args=?args.join(" "), env=?env, cwd=?cwd, "command_exec");
230 let result = std::process::Command::new(&program)
231 .args(args)
232 .stdin(stdio.get_process_stdio())
233 .stdout(stdio.get_process_stdio())
234 .envs(env.iter().map(|pair| (&pair.0, &pair.1)))
235 .current_dir(cwd.unwrap_or_else(|| Utf8Path::new("./").to_path_buf()))
236 .status();
237
238 match result {
239 Ok(status) => Ok(status.code().unwrap_or_default()),
240
241 Err(error) => Err(match error.kind() {
242 io::ErrorKind::NotFound => Error::ShellProgramNotFound {
243 program,
244 os: get_os(),
245 },
246
247 other => Error::ShellCommand {
248 program,
249 reason: ShellCommandFailureReason::IoError(other),
250 },
251 }),
252 }
253 }
254}
255
256impl BeamCompilerIO for ProjectIO {
257 fn compile_beam(
258 &self,
259 out: &Utf8Path,
260 lib: &Utf8Path,
261 modules: &HashSet<Utf8PathBuf>,
262 stdio: Stdio,
263 ) -> Result<Vec<String>, Error> {
264 let mut guard = self
265 .beam_compiler
266 .lock()
267 .expect("could not lock beam_compiler");
268
269 match &mut *guard {
270 Some(compiler) => compiler.compile(out, lib, modules, stdio),
271 None => {
272 let mut compiler = BeamCompilerInstance::new(self)?;
273 let result = compiler.compile(out, lib, modules, stdio);
274 *guard = Some(compiler);
275 result
276 }
277 }
278 }
279}
280
281impl MakeLocker for ProjectIO {
282 fn make_locker(&self, paths: &ProjectPaths, target: Target) -> Result<Box<dyn Locker>> {
283 let locker = LspLocker::new(paths, target)?;
284 Ok(Box::new(locker))
285 }
286}
287
288impl DownloadDependencies for ProjectIO {
289 fn download_dependencies(&self, paths: &ProjectPaths) -> Result<Manifest> {
290 dependencies::resolve_and_download(
291 paths,
292 NullTelemetry,
293 None,
294 Vec::new(),
295 dependencies::DependencyManagerConfig {
296 use_manifest: dependencies::UseManifest::Yes,
297 check_major_versions: dependencies::CheckMajorVersions::No,
298 },
299 )
300 }
301}
302
303pub fn delete_directory(dir: &Utf8Path) -> Result<(), Error> {
304 tracing::debug!(path=?dir, "deleting_directory");
305 if dir.exists() {
306 std::fs::remove_dir_all(dir).map_err(|e| Error::FileIo {
307 action: FileIoAction::Delete,
308 kind: FileKind::Directory,
309 path: dir.to_path_buf(),
310 err: Some(e.to_string()),
311 })?;
312 } else {
313 tracing::debug!(path=?dir, "directory_did_not_exist_for_deletion");
314 }
315 Ok(())
316}
317
318pub fn delete_file(file: &Utf8Path) -> Result<(), Error> {
319 tracing::debug!("Deleting file {:?}", file);
320 if file.exists() {
321 std::fs::remove_file(file).map_err(|e| Error::FileIo {
322 action: FileIoAction::Delete,
323 kind: FileKind::File,
324 path: file.to_path_buf(),
325 err: Some(e.to_string()),
326 })?;
327 } else {
328 tracing::debug!("Did not exist for deletion: {:?}", file);
329 }
330 Ok(())
331}
332
333pub fn write_outputs_under(outputs: &[OutputFile], base: &Utf8Path) -> Result<(), Error> {
334 for file in outputs {
335 let path = base.join(&file.path);
336 match &file.content {
337 Content::Binary(buffer) => write_bytes(&path, buffer),
338 Content::Text(buffer) => write(&path, buffer),
339 }?;
340 }
341 Ok(())
342}
343
344pub fn write_output(file: &OutputFile) -> Result<(), Error> {
345 let OutputFile { path, content } = file;
346 match content {
347 Content::Binary(buffer) => write_bytes(path, buffer),
348 Content::Text(buffer) => write(path, buffer),
349 }
350}
351
352pub fn write(path: &Utf8Path, text: &str) -> Result<(), Error> {
353 write_bytes(path, text.as_bytes())
354}
355
356#[cfg(target_family = "unix")]
357pub fn make_executable(path: impl AsRef<Utf8Path>) -> Result<(), Error> {
358 use std::os::unix::fs::PermissionsExt;
359 tracing::debug!(path = ?path.as_ref(), "setting_permissions");
360
361 std::fs::set_permissions(path.as_ref(), std::fs::Permissions::from_mode(0o755)).map_err(
362 |e| Error::FileIo {
363 action: FileIoAction::UpdatePermissions,
364 kind: FileKind::File,
365 path: path.as_ref().to_path_buf(),
366 err: Some(e.to_string()),
367 },
368 )?;
369 Ok(())
370}
371
372#[cfg(not(target_family = "unix"))]
373pub fn make_executable(_path: impl AsRef<Utf8Path>) -> Result<(), Error> {
374 Ok(())
375}
376
377pub fn write_bytes(path: &Utf8Path, bytes: &[u8]) -> Result<(), Error> {
378 tracing::debug!(path=?path, "writing_file");
379
380 let dir_path = path.parent().ok_or_else(|| Error::FileIo {
381 action: FileIoAction::FindParent,
382 kind: FileKind::Directory,
383 path: path.to_path_buf(),
384 err: None,
385 })?;
386
387 std::fs::create_dir_all(dir_path).map_err(|e| Error::FileIo {
388 action: FileIoAction::Create,
389 kind: FileKind::Directory,
390 path: dir_path.to_path_buf(),
391 err: Some(e.to_string()),
392 })?;
393
394 let mut f = File::create(path).map_err(|e| Error::FileIo {
395 action: FileIoAction::Create,
396 kind: FileKind::File,
397 path: path.to_path_buf(),
398 err: Some(e.to_string()),
399 })?;
400
401 f.write_all(bytes).map_err(|e| Error::FileIo {
402 action: FileIoAction::WriteTo,
403 kind: FileKind::File,
404 path: path.to_path_buf(),
405 err: Some(e.to_string()),
406 })?;
407 Ok(())
408}
409
410pub fn write_to_open_file(
411 file: &mut File,
412 path: &Utf8PathBuf,
413 data: impl AsRef<[u8]>,
414) -> Result<()> {
415 file.write_all(data.as_ref()).map_err(|e| Error::FileIo {
416 action: FileIoAction::WriteTo,
417 kind: FileKind::File,
418 path: path.clone(),
419 err: Some(e.to_string()),
420 })
421}
422
423fn is_gleam_path(path: &Utf8Path, dir: impl AsRef<Utf8Path>) -> bool {
424 use regex::Regex;
425
426 static RE: OnceLock<Regex> = OnceLock::new();
427
428 RE.get_or_init(|| {
429 Regex::new(&format!(
430 "^({module}{slash})*{module}\\.gleam$",
431 module = "[a-z][_a-z0-9]*",
432 slash = "(/|\\\\)",
433 ))
434 .expect("is_gleam_path() RE regex")
435 })
436 .is_match(
437 path.strip_prefix(dir.as_ref())
438 .expect("is_gleam_path(): strip_prefix")
439 .as_str(),
440 )
441}
442
443fn is_gleam_build_dir(e: &ignore::DirEntry) -> bool {
444 if !e.path().is_dir() || !e.path().ends_with("build") {
445 return false;
446 }
447
448 let Some(parent_path) = e.path().parent() else {
449 return false;
450 };
451
452 parent_path.join("gleam.toml").exists()
453}
454
455/// Walks through all Gleam module files in the directory, even if ignored,
456/// except for those in the `build/` directory. Excludes any Gleam files within
457/// invalid module paths, for example if they or a folder they're in contain a
458/// dot or a hyphen within their names.
459pub fn gleam_files(dir: &Utf8Path) -> impl Iterator<Item = Utf8PathBuf> + '_ {
460 ignore::WalkBuilder::new(dir)
461 .follow_links(true)
462 .standard_filters(false)
463 .filter_entry(|entry| !is_gleam_build_dir(entry))
464 .build()
465 .filter_map(Result::ok)
466 .filter(|entry| {
467 entry
468 .file_type()
469 .map(|type_| type_.is_file())
470 .unwrap_or(false)
471 })
472 .map(ignore::DirEntry::into_path)
473 .map(|path| Utf8PathBuf::from_path_buf(path).expect("Non Utf-8 Path"))
474 .filter(move |d| is_gleam_path(d, dir))
475}
476
477/// Walks through all native files in the directory, such as `.mjs` and `.erl`,
478/// even if ignored.
479pub fn native_files(dir: &Utf8Path) -> impl Iterator<Item = Utf8PathBuf> + '_ {
480 ignore::WalkBuilder::new(dir)
481 .follow_links(true)
482 .standard_filters(false)
483 .filter_entry(|entry| !is_gleam_build_dir(entry))
484 .build()
485 .filter_map(Result::ok)
486 .filter(|entry| {
487 entry
488 .file_type()
489 .map(|type_| type_.is_file())
490 .unwrap_or(false)
491 })
492 .map(ignore::DirEntry::into_path)
493 .map(|path| Utf8PathBuf::from_path_buf(path).expect("Non Utf-8 Path"))
494 .filter(|path| {
495 let extension = path.extension().unwrap_or_default();
496 is_native_file_extension(extension)
497 })
498}
499
500/// Walks through all files in the directory, even if ignored.
501pub fn priv_directory_files(dir: &Utf8Path) -> impl Iterator<Item = Utf8PathBuf> + '_ {
502 ignore::WalkBuilder::new(dir)
503 .follow_links(true)
504 .standard_filters(false)
505 .build()
506 .filter_map(Result::ok)
507 .filter(|entry| {
508 entry
509 .file_type()
510 .map(|type_| type_.is_file())
511 .unwrap_or(false)
512 })
513 .map(ignore::DirEntry::into_path)
514 .map(|path| Utf8PathBuf::from_path_buf(path).expect("Non Utf-8 Path"))
515}
516
517/// Walks through all `.erl` and `.hrl` files in the directory, even if ignored.
518pub fn erlang_files(dir: &Utf8Path) -> impl Iterator<Item = Utf8PathBuf> + '_ {
519 ignore::WalkBuilder::new(dir)
520 .follow_links(true)
521 .standard_filters(false)
522 .build()
523 .filter_map(Result::ok)
524 .filter(|entry| {
525 entry
526 .file_type()
527 .map(|type_| type_.is_file())
528 .unwrap_or(false)
529 })
530 .map(ignore::DirEntry::into_path)
531 .map(|path| Utf8PathBuf::from_path_buf(path).expect("Non Utf-8 Path"))
532 .filter(|path| {
533 let extension = path.extension().unwrap_or_default();
534 extension == "erl" || extension == "hrl"
535 })
536}
537
538pub fn create_tar_archive(outputs: Vec<OutputFile>) -> Result<Vec<u8>, Error> {
539 tracing::debug!("creating_tar_archive");
540
541 let encoder = flate2::write::GzEncoder::new(vec![], flate2::Compression::default());
542 let mut builder = tar::Builder::new(encoder);
543
544 for file in outputs {
545 let mut header = tar::Header::new_gnu();
546 header.set_path(&file.path).map_err(|e| Error::AddTar {
547 path: file.path.clone(),
548 err: e.to_string(),
549 })?;
550 header.set_size(file.content.as_bytes().len() as u64);
551 header.set_cksum();
552 builder
553 .append(&header, file.content.as_bytes())
554 .map_err(|e| Error::AddTar {
555 path: file.path.clone(),
556 err: e.to_string(),
557 })?;
558 }
559
560 builder
561 .into_inner()
562 .map_err(|e| Error::TarFinish(e.to_string()))?
563 .finish()
564 .map_err(|e| Error::Gzip(e.to_string()))
565}
566
567pub fn mkdir(path: impl AsRef<Utf8Path> + Debug) -> Result<(), Error> {
568 if path.as_ref().exists() {
569 return Ok(());
570 }
571
572 tracing::debug!(path=?path, "creating_directory");
573
574 std::fs::create_dir_all(path.as_ref()).map_err(|err| Error::FileIo {
575 kind: FileKind::Directory,
576 path: Utf8PathBuf::from(path.as_ref()),
577 action: FileIoAction::Create,
578 err: Some(err.to_string()),
579 })
580}
581
582pub fn read_dir(path: impl AsRef<Utf8Path> + Debug) -> Result<ReadDirUtf8, Error> {
583 tracing::debug!(path=?path,"reading_directory");
584
585 Utf8Path::read_dir_utf8(path.as_ref()).map_err(|e| Error::FileIo {
586 action: FileIoAction::Read,
587 kind: FileKind::Directory,
588 path: Utf8PathBuf::from(path.as_ref()),
589 err: Some(e.to_string()),
590 })
591}
592
593pub fn module_caches_paths(
594 path: impl AsRef<Utf8Path> + Debug,
595) -> Result<impl Iterator<Item = Utf8PathBuf>, Error> {
596 Ok(read_dir(path)?
597 .filter_map(Result::ok)
598 .map(|f| f.into_path())
599 .filter(|p| p.extension() == Some("cache")))
600}
601
602pub fn read(path: impl AsRef<Utf8Path>) -> Result<String, Error> {
603 let path = path.as_ref();
604 tracing::debug!(path=?path,"reading_file");
605
606 std::fs::read_to_string(path).map_err(|err| Error::FileIo {
607 action: FileIoAction::Read,
608 kind: FileKind::File,
609 path: Utf8PathBuf::from(path),
610 err: Some(err.to_string()),
611 })
612}
613
614pub fn open_file(path: impl AsRef<Utf8Path>) -> Result<File, Error> {
615 let path = path.as_ref();
616 tracing::debug!(path=?path,"opening_file");
617
618 File::create(path).map_err(|err| Error::FileIo {
619 action: FileIoAction::Open,
620 kind: FileKind::File,
621 path: Utf8PathBuf::from(path),
622 err: Some(err.to_string()),
623 })
624}
625
626pub fn read_bytes(path: impl AsRef<Utf8Path>) -> Result<Vec<u8>, Error> {
627 let path = path.as_ref();
628 tracing::debug!(path=?path,"reading_file");
629
630 std::fs::read(path).map_err(|err| Error::FileIo {
631 action: FileIoAction::Read,
632 kind: FileKind::File,
633 path: Utf8PathBuf::from(path),
634 err: Some(err.to_string()),
635 })
636}
637
638pub fn reader(path: impl AsRef<Utf8Path>) -> Result<WrappedReader, Error> {
639 let path = path.as_ref();
640 tracing::debug!(path=?path,"opening_file_reader");
641
642 let reader = File::open(path).map_err(|err| Error::FileIo {
643 action: FileIoAction::Open,
644 kind: FileKind::File,
645 path: Utf8PathBuf::from(path),
646 err: Some(err.to_string()),
647 })?;
648
649 Ok(WrappedReader::new(path, Box::new(reader)))
650}
651
652pub fn buffered_reader<P: AsRef<Utf8Path>>(path: P) -> Result<impl BufRead, Error> {
653 let path = path.as_ref();
654 tracing::debug!(path=?path,"opening_file_buffered_reader");
655 let reader = File::open(path).map_err(|err| Error::FileIo {
656 action: FileIoAction::Open,
657 kind: FileKind::File,
658 path: Utf8PathBuf::from(path),
659 err: Some(err.to_string()),
660 })?;
661 Ok(BufReader::new(reader))
662}
663
664pub fn copy(path: impl AsRef<Utf8Path>, to: impl AsRef<Utf8Path>) -> Result<(), Error> {
665 let path = path.as_ref();
666 let to = to.as_ref();
667 tracing::debug!(from=?path, to=?to, "copying_file");
668
669 // TODO: include the destination in the error message
670 std::fs::copy(path, to)
671 .map_err(|err| Error::FileIo {
672 action: FileIoAction::Copy,
673 kind: FileKind::File,
674 path: Utf8PathBuf::from(path),
675 err: Some(err.to_string()),
676 })
677 .map(|_| ())
678}
679
680pub fn copy_dir(path: impl AsRef<Utf8Path>, to: impl AsRef<Utf8Path>) -> Result<(), Error> {
681 let path = path.as_ref();
682 let to = to.as_ref();
683 tracing::debug!(from=?path, to=?to, "copying_directory");
684
685 // TODO: include the destination in the error message
686 fs_extra::dir::copy(
687 path,
688 to,
689 &fs_extra::dir::CopyOptions::new()
690 .copy_inside(false)
691 .content_only(true),
692 )
693 .map_err(|err| Error::FileIo {
694 action: FileIoAction::Copy,
695 kind: FileKind::Directory,
696 path: Utf8PathBuf::from(path),
697 err: Some(err.to_string()),
698 })
699 .map(|_| ())
700}
701
702pub fn symlink_dir(src: impl AsRef<Utf8Path>, dest: impl AsRef<Utf8Path>) -> Result<(), Error> {
703 let src = src.as_ref();
704 let dest = dest.as_ref();
705 tracing::debug!(src=?src, dest=?dest, "symlinking");
706 let src = canonicalise(src)?;
707
708 #[cfg(target_family = "windows")]
709 let result = std::os::windows::fs::symlink_dir(src, dest);
710 #[cfg(not(target_family = "windows"))]
711 let result = std::os::unix::fs::symlink(src, dest);
712
713 result.map_err(|err| Error::FileIo {
714 action: FileIoAction::Link,
715 kind: FileKind::File,
716 path: Utf8PathBuf::from(dest),
717 err: Some(err.to_string()),
718 })?;
719 Ok(())
720}
721
722pub fn hardlink(from: impl AsRef<Utf8Path>, to: impl AsRef<Utf8Path>) -> Result<(), Error> {
723 let from = from.as_ref();
724 let to = to.as_ref();
725 tracing::debug!(from=?from, to=?to, "hardlinking");
726 std::fs::hard_link(from, to)
727 .map_err(|err| Error::FileIo {
728 action: FileIoAction::Link,
729 kind: FileKind::File,
730 path: Utf8PathBuf::from(from),
731 err: Some(err.to_string()),
732 })
733 .map(|_| ())
734}
735
736/// Check if the given path is inside a git work tree.
737/// This is done by running `git rev-parse --is-inside-work-tree --quiet` in the
738/// given path. If git is not installed then we assume we're not in a git work
739/// tree.
740///
741fn is_inside_git_work_tree(path: &Utf8Path) -> Result<bool, Error> {
742 tracing::debug!(path=?path, "checking_for_git_repo");
743
744 let args: Vec<&str> = vec!["rev-parse", "--is-inside-work-tree", "--quiet"];
745
746 // Ignore all output, rely on the exit code instead.
747 // git will display a fatal error on stderr if rev-parse isn't run inside of a git work tree,
748 // so send stderr to /dev/null
749 let result = std::process::Command::new("git")
750 .args(args)
751 .stdin(std::process::Stdio::null())
752 .stdout(std::process::Stdio::null())
753 .stderr(std::process::Stdio::null())
754 .current_dir(path)
755 .status();
756
757 match result {
758 Ok(status) => Ok(status.success()),
759 Err(error) => match error.kind() {
760 io::ErrorKind::NotFound => Ok(false),
761
762 other => Err(Error::ShellCommand {
763 program: "git".into(),
764 reason: ShellCommandFailureReason::IoError(other),
765 }),
766 },
767 }
768}
769
770pub(crate) fn is_git_work_tree_root(path: &Utf8Path) -> bool {
771 tracing::debug!(path=?path, "checking_for_git_repo_root");
772 exists(path.join(".git")).unwrap_or(false)
773}
774
775/// Run `git init` in the given path.
776/// If git is not installed then we do nothing.
777pub fn git_init(path: &Utf8Path) -> Result<(), Error> {
778 tracing::debug!(path=?path, "initializing git");
779
780 if is_inside_git_work_tree(path)? {
781 tracing::debug!(path=?path, "git_repo_already_exists");
782 return Ok(());
783 }
784
785 let args = vec!["init".into(), "--quiet".into(), path.to_string()];
786
787 let command = Command {
788 program: "git".to_string(),
789 args,
790 env: vec![],
791 cwd: None,
792 stdio: Stdio::Inherit,
793 };
794 match ProjectIO::new().exec(command) {
795 Ok(_) => Ok(()),
796 Err(err) => match err {
797 Error::ShellProgramNotFound { .. } => Ok(()),
798 _ => Err(Error::GitInitialization {
799 error: err.to_string(),
800 }),
801 },
802 }
803}
804
805pub fn canonicalise(path: &Utf8Path) -> Result<Utf8PathBuf, Error> {
806 std::fs::canonicalize(path)
807 .map_err(|err| Error::FileIo {
808 action: FileIoAction::Canonicalise,
809 kind: FileKind::File,
810 path: Utf8PathBuf::from(path),
811 err: Some(err.to_string()),
812 })
813 .map(|pb| Utf8PathBuf::from_path_buf(pb).expect("Non Utf8 Path"))
814}
815
816#[derive(Debug, Clone, Copy)]
817pub struct ConsoleWarningEmitter;
818
819impl WarningEmitterIO for ConsoleWarningEmitter {
820 fn emit_warning(&self, warning: Warning) {
821 let buffer_writer = crate::cli::stderr_buffer_writer();
822 let mut buffer = buffer_writer.buffer();
823 warning.pretty(&mut buffer);
824 buffer_writer
825 .print(&buffer)
826 .expect("Writing warning to stderr");
827 }
828}
829
830/// Returns root of Git repository in base path if it is initialised.
831pub fn get_git_repository_root(mut path: Utf8PathBuf) -> Option<Utf8PathBuf> {
832 loop {
833 if path.join(".git").is_dir() {
834 return Some(path);
835 }
836
837 path = match path.parent() {
838 Some(path) => path.into(),
839 None => return None,
840 }
841 }
842}
843
844#[derive(Debug)]
845pub struct ZipArchive<W: Write + io::Seek> {
846 zip: zip::ZipWriter<W>,
847}
848
849impl<W: Write + io::Seek> ZipArchive<W> {
850 pub fn new(writer: W) -> Self {
851 Self {
852 zip: zip::ZipWriter::new(writer),
853 }
854 }
855
856 pub fn finish(self) -> Result<W> {
857 self.zip
858 .finish()
859 .map_err(|e| Error::ZipFinish(e.to_string()))
860 }
861
862 pub fn add_file_from_disc(
863 &mut self,
864 disc_path: impl AsRef<Utf8Path>,
865 zip_path: impl Into<String>,
866 ) -> Result<()> {
867 let disc_path = disc_path.as_ref();
868 let zip_path = zip_path.into();
869 self.zip
870 .start_file(zip_path.clone(), self.options())
871 .map_err(|e| Error::ZipAdd {
872 path: zip_path,
873 error: e.to_string(),
874 })?;
875 let mut file = File::open(disc_path).map_err(|e| Error::FileIo {
876 kind: FileKind::File,
877 action: FileIoAction::Open,
878 path: disc_path.to_path_buf(),
879 err: Some(e.to_string()),
880 })?;
881 let _: u64 = io::copy(&mut file, &mut self.zip).map_err(|e| Error::FileIo {
882 kind: FileKind::File,
883 action: FileIoAction::Copy,
884 path: disc_path.to_path_buf(),
885 err: Some(e.to_string()),
886 })?;
887 Ok(())
888 }
889
890 fn options(&self) -> zip::write::FileOptions<'static, ()> {
891 zip::write::SimpleFileOptions::default()
892 }
893}