aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2017-06-14 17:56:49 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2017-06-15 21:08:07 +0300
commitfc8c0e410697928d644f06dea772b51c3abc0e3f (patch)
treee136e0d5e3df77ef65818ea94fee1bdced9a074f
parent87f97db9d5dbe2aefbeb6c49c4bb65aa3b8afbc7 (diff)
Add support for ln testscript builtin
-rw-r--r--build2/cc/windows-rpath.cxx22
-rw-r--r--build2/test/script/builtin.cxx231
-rw-r--r--doc/testscript.cli39
-rw-r--r--tests/test/script/builtin/buildfile2
-rw-r--r--tests/test/script/builtin/ln.test184
5 files changed, 461 insertions, 17 deletions
diff --git a/build2/cc/windows-rpath.cxx b/build2/cc/windows-rpath.cxx
index 0078944..450a0f5 100644
--- a/build2/cc/windows-rpath.cxx
+++ b/build2/cc/windows-rpath.cxx
@@ -315,13 +315,13 @@ namespace build2
}
catch (const system_error& e)
{
- // Make sure that the error denotes errno portable code.
+ // Note that we are not guaranteed (here and below) that the
+ // system_error exception is of the generic category.
//
- assert (e.code ().category () == generic_category ());
-
int c (e.code ().value ());
-
- if (c != EPERM && c != ENOSYS)
+ if (!(e.code ().category () == generic_category () &&
+ (c == ENOSYS || // Not implemented.
+ c == EPERM))) // Not supported by the filesystem(s).
{
print ("ln -s");
fail << "unable to create symlink " << l << ": " << e;
@@ -334,13 +334,11 @@ namespace build2
}
catch (const system_error& e)
{
- // Make sure the error reflects errno portable code.
- //
- assert (e.code ().category () == generic_category ());
-
- int c (e.code ().value ());
-
- if (c != EPERM && c != ENOSYS)
+ c = e.code ().value ();
+ if (!(e.code ().category () == generic_category () &&
+ (c == ENOSYS || // Not implemented.
+ c == EPERM || // Not supported by the filesystem(s).
+ c == EXDEV))) // On different filesystems.
{
print ("ln");
fail << "unable to create hardlink " << l << ": " << e;
diff --git a/build2/test/script/builtin.cxx b/build2/test/script/builtin.cxx
index 079a12d..42e02d8 100644
--- a/build2/test/script/builtin.cxx
+++ b/build2/test/script/builtin.cxx
@@ -264,6 +264,7 @@ namespace build2
static void
cpfile (scope& sp,
const path& from, const path& to,
+ bool overwrite,
bool cleanup,
const function<error_record()>& fail)
{
@@ -272,7 +273,9 @@ namespace build2
bool exists (file_exists (to));
cpfile (from, to,
- cpflags::overwrite_permissions | cpflags::overwrite_content);
+ overwrite
+ ? cpflags::overwrite_permissions | cpflags::overwrite_content
+ : cpflags::none);
if (!exists && cleanup)
sp.clean ({cleanup_type::always, to}, true);
@@ -315,7 +318,7 @@ namespace build2
cleanup,
fail);
else
- cpfile (sp, f, t, cleanup, fail);
+ cpfile (sp, f, t, false, cleanup, fail);
}
}
catch (const system_error& e)
@@ -410,7 +413,7 @@ namespace build2
if (!recursive)
// Synopsis 1: make a file copy at the specified path.
//
- cpfile (sp, src, dst, cleanup, fail);
+ cpfile (sp, src, dst, true, cleanup, fail);
else
// Synopsis 2: make a directory copy at the specified path.
//
@@ -439,7 +442,7 @@ namespace build2
// Synopsis 3: copy a file into the specified directory. Also,
// here we cover synopsis 4 for the source path being a file.
//
- cpfile (sp, src, dst / src.leaf (), cleanup, fail);
+ cpfile (sp, src, dst / src.leaf (), true, cleanup, fail);
}
}
@@ -530,6 +533,225 @@ namespace build2
return builtin (r = 0);
}
+ // Create a symlink to a file or directory at the specified path. The
+ // paths must be absolute. Fall back to creating a hardlink, if symlink
+ // creation is not supported for the link path. If hardlink creation is
+ // not supported either, then fall back to copies. If requested, created
+ // filesystem entries are registered for cleanup. Fail if the target
+ // filesystem entry doesn't exist or an exception is thrown by the
+ // underlying filesystem operation (specifically for an already existing
+ // filesystem entry at the link path).
+ //
+ // Note that supporting optional removal of an existing filesystem entry
+ // at the link path (the -f option) tends to get hairy. As soon as an
+ // existing and the resulting filesystem entries could be of different
+ // types, we would end up with canceling an old cleanup and registering
+ // the new one. Also removing non-empty directories doesn't look very
+ // natural, but would be required if we want the behavior on POSIX and
+ // Windows to be consistent.
+ //
+ static void
+ mksymlink (scope& sp,
+ const path& target, const path& link,
+ bool cleanup,
+ const function<error_record()>& fail)
+ {
+ // Determine the target type, fail if the target doesn't exist.
+ //
+ bool dir (false);
+
+ try
+ {
+ pair<bool, entry_stat> pe (path_entry (target));
+
+ if (!pe.first)
+ fail () << "unable to create symlink to '" << target << "': "
+ << "no such file or directory";
+
+ dir = pe.second.type == entry_type::directory;
+ }
+ catch (const system_error& e)
+ {
+ fail () << "unable to stat '" << target << "': " << e;
+ }
+
+ // First we try to create a symlink. If that fails (e.g., "Windows
+ // happens"), then we resort to hard links. If that doesn't work out
+ // either (e.g., not on the same filesystem), then we fall back to
+ // copies. So things are going to get a bit nested.
+ //
+ try
+ {
+ mksymlink (target, link, dir);
+
+ if (cleanup)
+ sp.clean ({cleanup_type::always, link}, true);
+ }
+ catch (const system_error& e)
+ {
+ // Note that we are not guaranteed (here and below) that the
+ // system_error exception is of the generic category.
+ //
+ int c (e.code ().value ());
+ if (!(e.code ().category () == generic_category () &&
+ (c == ENOSYS || // Not implemented.
+ c == EPERM))) // Not supported by the filesystem(s).
+ fail () << "unable to create symlink '" << link << "' to '"
+ << target << "': " << e;
+
+ try
+ {
+ mkhardlink (target, link, dir);
+
+ if (cleanup)
+ sp.clean ({cleanup_type::always, link}, true);
+ }
+ catch (const system_error& e)
+ {
+ c = e.code ().value ();
+ if (!(e.code ().category () == generic_category () &&
+ (c == ENOSYS || // Not implemented.
+ c == EPERM || // Not supported by the filesystem(s).
+ c == EXDEV))) // On different filesystems.
+ fail () << "unable to create hardlink '" << link << "' to '"
+ << target << "': " << e;
+
+ if (dir)
+ cpdir (sp,
+ path_cast<dir_path> (target), path_cast<dir_path> (link),
+ cleanup,
+ fail);
+ else
+ cpfile (sp, target, link, false, cleanup, fail);
+ }
+ }
+ }
+
+ // ln [--no-cleanup] -s <target-path> <link-path>
+ // ln [--no-cleanup] -s <target-path>... <link-dir>/
+ //
+ // Note: can be executed synchronously.
+ //
+ static uint8_t
+ ln (scope& sp,
+ const strings& args,
+ auto_fd in, auto_fd out, auto_fd err) noexcept
+ try
+ {
+ uint8_t r (1);
+ ofdstream cerr (move (err));
+
+ auto error = [&cerr] (bool fail = true)
+ {
+ return error_record (cerr, fail, "ln");
+ };
+
+ try
+ {
+ in.close ();
+ out.close ();
+
+ auto i (args.begin ());
+ auto e (args.end ());
+
+ // Process options.
+ //
+ bool cleanup (true);
+ bool symlink (false);
+
+ for (; i != e; ++i)
+ {
+ const string& o (*i);
+
+ if (o == "--no-cleanup")
+ cleanup = false;
+ else if (o == "-s")
+ symlink = true;
+ else
+ {
+ if (o == "--")
+ ++i;
+
+ break;
+ }
+ }
+
+ if (!symlink)
+ error () << "missing -s option";
+
+ // Create file or directory symlinks.
+ //
+ if (i == e)
+ error () << "missing arguments";
+
+ const dir_path& wd (sp.wd_path);
+
+ auto j (args.rbegin ());
+ path link (parse_path (*j++, wd));
+ e = j.base ();
+
+ if (i == e)
+ error () << "missing target path";
+
+ auto fail = [&error] () {return error (true);};
+
+ // If link is not a directory path (no trailing separator), then
+ // create a symlink to the target path at the specified link path
+ // (the only target path is allowed in such a case). Otherwise create
+ // links to the target paths inside the specified directory.
+ //
+ if (!link.to_directory ())
+ {
+ path target (parse_path (*i++, wd));
+
+ // If there are multiple targets but no trailing separator for the
+ // link, then, most likelly, it is missing.
+ //
+ if (i != e)
+ error () << "multiple target paths with non-directory link path";
+
+ // Synopsis 1: create a target path symlink at the specified path.
+ //
+ mksymlink (sp, target, link, cleanup, fail);
+ }
+ else
+ {
+ for (; i != e; ++i)
+ {
+ path target (parse_path (*i, wd));
+
+ // Synopsis 2: create a target path symlink in the specified
+ // directory.
+ //
+ mksymlink (sp, target, link / target.leaf (), cleanup, fail);
+ }
+ }
+
+ r = 0;
+ }
+ catch (const invalid_path& e)
+ {
+ error (false) << "invalid path '" << e.path << "'";
+ }
+ // Can be thrown while closing in, out or writing to cerr.
+ //
+ catch (const io_error& e)
+ {
+ error (false) << e;
+ }
+ catch (const failed&)
+ {
+ // Diagnostics has already been issued.
+ }
+
+ cerr.close ();
+ return r;
+ }
+ catch (const std::exception&)
+ {
+ return 1;
+ }
+
// Create a directory if not exist and its parent directories if
// necessary. Throw system_error on failure. Register created
// directories for cleanup. The directory path must be absolute.
@@ -1397,6 +1619,7 @@ namespace build2
{"cp", &sync_impl<&cp>},
{"echo", &async_impl<&echo>},
{"false", &false_},
+ {"ln", &sync_impl<&ln>},
{"mkdir", &sync_impl<&mkdir>},
{"rm", &sync_impl<&rm>},
{"rmdir", &sync_impl<&rmdir>},
diff --git a/doc/testscript.cli b/doc/testscript.cli
index 685915e..c039ec0 100644
--- a/doc/testscript.cli
+++ b/doc/testscript.cli
@@ -2342,6 +2342,45 @@ false
Do nothing and terminate normally with the 1 exit code (indicating failure).
+
+\h#builtins-ln|\c{ln}|
+
+\
+ln [--no-cleanup] -s <target-path> <link-path>
+ln [--no-cleanup] -s <target-path>... <dir>/
+\
+
+Create symbolic links to files and/or directories. The first form creates a
+single target link at the specified path. The second form creates links to one
+or more targets inside the specified directory.
+
+If the last argument does not end with a directory separator, then the first
+synopsis is assumed where \c{ln} creates the symbolic link to \i{target-path}
+at \i{link-path} failing if the \i{target-path} filesystem entry does not
+exist, \i{link-path} filesystem entry already exists or more than two arguments
+are specified.
+
+If the last argument ends with a directory separator, then the second synopsis
+is assumed where \c{ln} creates one or more symbolic links to \i{target-path}
+files or directories inside the \i{dir} directory as if by executing the
+following command for each target:
+
+\
+ln -s target-path dir/target-name
+\
+
+Where \i{target-name} is the last path component in \i{target-path}.
+
+For both cases \c{ln} falls back to creating a hard link if symbolic link
+creation is not supported. If hard link creation is not supported either,
+then \c{ln} falls back to copying the content, recursively in case of a
+directory target.
+
+Unless the --no-cleanup option is specified, created filesystem entries that
+are inside the script working directory are automatically registered for
+cleanup.
+
+
\h#builtins-mkdir|\c{mkdir}|
\
diff --git a/tests/test/script/builtin/buildfile b/tests/test/script/builtin/buildfile
index f72688d..43649db 100644
--- a/tests/test/script/builtin/buildfile
+++ b/tests/test/script/builtin/buildfile
@@ -2,4 +2,4 @@
# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
# license : MIT; see accompanying LICENSE file
-./: test{cat cp echo mkdir rm rmdir sed test touch} $b
+./: test{*} $b
diff --git a/tests/test/script/builtin/ln.test b/tests/test/script/builtin/ln.test
new file mode 100644
index 0000000..99e2bcb
--- /dev/null
+++ b/tests/test/script/builtin/ln.test
@@ -0,0 +1,184 @@
+# file : tests/test/script/builtin/ln.test
+# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+.include ../common.test
+
+: args
+:
+{
+ : -s-option
+ :
+ $c <'ln 2>"ln: missing -s option" == 1' && $b
+
+ : none
+ :
+ $c <'ln -s 2>"ln: missing arguments" == 1' && $b
+
+ : no-target
+ :
+ $c <'ln -s a 2>"ln: missing target path" == 1' && $b
+
+ : no-trailing-sep
+ :
+ $c <<EOI && $b
+ ln -s a b c 2>"ln: multiple target paths with non-directory link path" == 1
+ EOI
+
+ : empty
+ :
+ {
+ : link
+ :
+ $c <<EOI && $b
+ ln -s '' 2>"ln: invalid path ''" == 1
+ EOI
+
+ : target1
+ :
+ $c <<EOI && $b
+ ln -s '' a 2>"ln: invalid path ''" == 1
+ EOI
+
+ : target2
+ :
+ $c <<EOI && $b
+ ln -s '' a b/ 2>"ln: invalid path ''" == 1
+ EOI
+ }
+}
+
+: file
+:
+: Test creating a file symlink.
+:
+{
+ : non-existing-link-path
+ :
+ $c <<EOI && $b
+ touch a;
+ ln -s a b && test -f b
+ EOI
+
+ : existing-link
+ :
+ {
+ : file
+ :
+ $c <<EOI && $b
+ touch a b;
+ ln -s a b 2>>/~%EOE% != 0
+ %(
+ %ln: unable to create .+link '.+/b' to '.+/a': .+%|
+ %ln: unable to copy file '.+/a' to '.+/b': .+%
+ %)
+ EOE
+ EOI
+
+ : dir
+ :
+ $c <<EOI && $b
+ touch a;
+ mkdir b;
+ ln -s a b 2>>/~%EOE% != 0
+ %(
+ %ln: unable to create .+link '.+/b' to '.+/a': .+%|
+ %ln: unable to copy file '.+/a' to '.+/b': .+%
+ %)
+ EOE
+ EOI
+ }
+
+ : non-existing
+ {
+ : target
+ :
+ $c <<EOI && $b
+ ln -s a b 2>>/~%EOE% != 0
+ %ln: unable to create symlink to '.+/a': no such file or directory%
+ EOE
+ EOI
+
+ : link-dir
+ :
+ $c <<EOI && $b
+ touch a;
+ ln -s a b/c 2>>/~%EOE% != 0
+ %(
+ %ln: unable to create .+link '.+/b/c' to '.+/a': .+%|
+ %ln: unable to copy file '.+/a' to '.+/b/c': .+%
+ %)
+ EOE
+ EOI
+ }
+}
+
+: dir
+:
+: Test creating a directory symlink.
+:
+{
+ : non-existing-link-path
+ :
+ $c <<EOI && $b
+ mkdir a;
+ touch a/b;
+ ln -s a c && test -f c/b
+ EOI
+
+ : existing-link
+ :
+ {
+ : dir
+ :
+ $c <<EOI && $b
+ mkdir a b;
+ ln -s a b 2>>/~%EOE% != 0
+ %(
+ %ln: unable to create .+link '.+/b' to '.+/a': .+%|
+ %ln: unable to copy directory '.+/a' to '.+/b': .+%
+ %)
+ EOE
+ EOI
+
+ : file
+ :
+ $c <<EOI && $b
+ mkdir a;
+ touch b;
+ ln -s a b 2>>/~%EOE% != 0
+ %(
+ %ln: unable to create .+link '.+/b' to '.+/a': .+%|
+ %ln: unable to copy directory '.+/a' to '.+/b': .+%
+ %)
+ EOE
+ EOI
+ }
+
+ : non-existing
+ {
+ : link-dir
+ :
+ $c <<EOI && $b
+ mkdir a;
+ ln -s a b/c 2>>/~%EOE% != 0
+ %(
+ %ln: unable to create .+link '.+/b/c' to '.+/a': .+%|
+ %ln: unable to copy directory '.+/a' to '.+/b/c': .+%
+ %)
+ EOE
+ EOI
+ }
+}
+
+: multiple-targets
+:
+: Test creating links for multiple targets in the specified directory.
+:
+{
+ $c <<EOI && $b
+ touch a;
+ mkdir b c;
+ ln -s a b c/ && test -f c/a && test -d c/b
+ EOI
+}