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