feat: IO.FS.hardLink (#10676)

This PR adds the `IO.FS.hardLink` function, which can be used to create
hard links.

This is implemented via libuv's `uv_fs_link` function.

Lake hopes to make use of this function to decrease the storage cost of
restoring artifacts.

This PR also fixes some C implementation issues found in nearby similar
functions.
This commit is contained in:
Mac Malone 2025-10-06 14:22:07 -04:00 committed by GitHub
parent 4898f28c12
commit 43d4c8fe9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 51 additions and 1 deletions

View file

@ -858,6 +858,18 @@ function](https://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html
-/
@[extern "lean_io_rename"] opaque rename (old new : @& FilePath) : IO Unit
/--
Creates a new hard link.
The `link` path will be a link pointing to the `orig` path.
Note that systems often require these two paths to both be located on the same filesystem.
If `orig` names a symbolic link, it is platform-specific whether the symbolic link is followed.
This function coincides with the [POSIX `link`
function](https://pubs.opengroup.org/onlinepubs/9699919799/functions/link.html).
-/
@[extern "lean_io_hard_link"] opaque hardLink (orig link : @& FilePath) : IO Unit
/--
Creates a temporary file in the most secure manner possible, returning both a `Handle` to the
already-opened file and its path.

View file

@ -1172,7 +1172,6 @@ extern "C" LEAN_EXPORT obj_res lean_io_rename(b_obj_arg from, b_obj_arg to, lean
}
const char* to_str = string_cstr(to);
if (strlen(to_str) != lean_string_size(to) - 1) {
inc(to);
return mk_embedded_nul_error(to);
}
#ifdef LEAN_WINDOWS
@ -1197,6 +1196,26 @@ extern "C" LEAN_EXPORT obj_res lean_io_rename(b_obj_arg from, b_obj_arg to, lean
return io_result_mk_ok(box(0));
}
/* hardLink (orig link : @& FilePath) : IO Unit */
extern "C" LEAN_EXPORT obj_res lean_io_hard_link(b_obj_arg orig, b_obj_arg link, lean_object * /* w */) {
const char* orig_str = string_cstr(orig);
if (strlen(orig_str) != lean_string_size(orig) - 1) {
return mk_embedded_nul_error(orig);
}
const char* link_str = string_cstr(link);
if (strlen(link_str) != lean_string_size(link) - 1) {
return mk_embedded_nul_error(link);
}
uv_fs_t req;
int ret = uv_fs_link(NULL, &req, orig_str, link_str, NULL);
uv_fs_req_cleanup(&req);
if (ret < 0) {
return io_result_mk_error(decode_uv_error(ret, orig));
} else {
return io_result_mk_ok(box(0));
}
}
/* createTempFile : IO (Handle × FilePath) */
extern "C" LEAN_EXPORT obj_res lean_io_create_tempfile(lean_object * /* w */) {
char path[PATH_MAX];
@ -1232,6 +1251,7 @@ extern "C" LEAN_EXPORT obj_res lean_io_create_tempfile(lean_object * /* w */) {
// Differences from lean_io_create_tempdir start here
ret = uv_fs_mkstemp(NULL, &req, path, NULL);
if (ret < 0) {
uv_fs_req_cleanup(&req);
// If mkstemp throws an error we cannot rely on path to contain a proper file name.
return io_result_mk_error(decode_uv_error(ret, nullptr));
} else {
@ -1277,6 +1297,7 @@ extern "C" LEAN_EXPORT obj_res lean_io_create_tempdir(lean_object * /* w */) {
// Differences from lean_io_create_tempfile start here
ret = uv_fs_mkdtemp(NULL, &req, path, NULL);
if (ret < 0) {
uv_fs_req_cleanup(&req);
// If mkdtemp throws an error we cannot rely on path to contain a proper file name.
return io_result_mk_error(decode_uv_error(ret, nullptr));
} else {

View file

@ -154,3 +154,20 @@ def testRemoveDirAll : IO Unit := do
assert! (← System.FilePath.pathExists "io_test/symlink_target")
#eval testRemoveDirAll
def testHardLink : IO Unit := do
let fn := "io_test/hardLinkTarget.txt"
let contents := "foo"
writeFile fn contents
let linkFn := "io_test/hardLink.txt"
if (← System.FilePath.pathExists linkFn) then
removeFile linkFn
hardLink fn linkFn
removeFile fn
assert! !(← System.FilePath.pathExists fn)
assert! (← System.FilePath.pathExists linkFn)
let linkContents ← readFile linkFn
check_eq "1" contents linkContents
#guard_msgs in
#eval testHardLink