diff options
-rw-r--r-- | build2/cc/windows-rpath.cxx | 22 | ||||
-rw-r--r-- | build2/test/script/builtin.cxx | 231 | ||||
-rw-r--r-- | doc/testscript.cli | 39 | ||||
-rw-r--r-- | tests/test/script/builtin/buildfile | 2 | ||||
-rw-r--r-- | tests/test/script/builtin/ln.test | 184 |
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 +} |