diff --git a/src/base/os.lua b/src/base/os.lua index 7ed36c5ca2..40db2b3ecf 100644 --- a/src/base/os.lua +++ b/src/base/os.lua @@ -575,21 +575,24 @@ local builtin_rmdir = os.rmdir function os.rmdir(p) - -- recursively remove subdirectories - local dirs = os.matchdirs(p .. "/*") - for _, dname in ipairs(dirs) do - local ok, err = os.rmdir(dname) - if not ok then - return ok, err + -- Only delete children if the path is not a symlink + if not os.islink(p) then + -- recursively remove subdirectories + local dirs = os.matchdirs(p .. "/*") + for _, dname in ipairs(dirs) do + local ok, err = os.rmdir(dname) + if not ok then + return ok, err + end end - end - -- remove any files - local files = os.matchfiles(p .. "/*") - for _, fname in ipairs(files) do - local ok, err = os.remove(fname) - if not ok then - return ok, err + -- remove any files + local files = os.matchfiles(p .. "/*") + for _, fname in ipairs(files) do + local ok, err = os.remove(fname) + if not ok then + return ok, err + end end end @@ -633,6 +636,12 @@ echo = function(v) return "echo " .. v end, + linkdir = function(v) + return "ln -s " .. path.normalize(v) + end, + linkfile = function(v) + return "ln -s " .. path.normalize(v) + end, mkdir = function(v) return "mkdir -p " .. path.normalize(v) end, @@ -678,6 +687,12 @@ echo = function(v) return "echo " .. v end, + linkdir = function(v) + return "mklink /d " .. path.translate(path.normalize(v)) + end, + linkfile = function(v) + return "mklink " .. path.translate(path.normalize(v)) + end, mkdir = function(v) v = path.translate(path.normalize(v)) return "IF NOT EXIST " .. v .. " (mkdir " .. v .. ")" diff --git a/src/host/os_linkdir.c b/src/host/os_linkdir.c new file mode 100644 index 0000000000..2d93718f59 --- /dev/null +++ b/src/host/os_linkdir.c @@ -0,0 +1,69 @@ +/** + * \file os_linkdir.c + * \brief Creates a symbolic link to a directory. + * \author Copyright (c) 2024 Jess Perkins and the Premake project + */ + +#include +#include "premake.h" + +int do_linkdir(lua_State* L, const char* src, const char* dst) +{ +#if PLATFORM_WINDOWS + // Prepend the drive letter if a relative path is given + char dstPath[MAX_PATH]; + char srcPath[MAX_PATH]; + + do_normalize(L, srcPath, src); + do_normalize(L, dstPath, dst); + do_translate(dstPath, '\\'); + do_translate(srcPath, '\\'); + + // Promote to wide path + wchar_t wSrcPath[MAX_PATH]; + wchar_t wDstPath[MAX_PATH]; + + MultiByteToWideChar(CP_UTF8, 0, srcPath, -1, wSrcPath, MAX_PATH); + MultiByteToWideChar(CP_UTF8, 0, dstPath, -1, wDstPath, MAX_PATH); + + // If the source path is relative, prepend the current working directory + if (!do_isabsolute(src)) + { + // Get the current working directory + wchar_t cwd[MAX_PATH]; + GetCurrentDirectoryW(MAX_PATH, cwd); + + // Convert the source path to a relative path + wchar_t relSrcPath[MAX_PATH]; + swprintf(relSrcPath, MAX_PATH, L"%c:%s", cwd[0], wSrcPath); + + BOOLEAN res = CreateSymbolicLinkW(wDstPath, relSrcPath, SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE); + return res != 0; + } + else + { + BOOLEAN res = CreateSymbolicLinkW(wDstPath, wSrcPath, SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE); + return res != 0; + } +#else + int res = symlink(src, dst); + return res == 0; +#endif +} + +int os_linkdir(lua_State* L) +{ + const char* src = luaL_checkstring(L, 1); + const char* dst = luaL_checkstring(L, 2); + + int result = do_linkdir(L, src, dst); + if (!result) + { + lua_pushnil(L); + lua_pushfstring(L, "Unable to create link from '%s' to '%s'", src, dst); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} diff --git a/src/host/os_linkfile.c b/src/host/os_linkfile.c new file mode 100644 index 0000000000..6f9e2954e8 --- /dev/null +++ b/src/host/os_linkfile.c @@ -0,0 +1,68 @@ +/** + * \file os_linkfile.c + * \brief Creates a symbolic link to a file. + * \author Copyright (c) 2024 Jess Perkins and the Premake project + */ + +#include +#include "premake.h" + +int do_linkfile(lua_State* L, const char* src, const char* dst) +{ +#if PLATFORM_WINDOWS + char dstPath[MAX_PATH]; + char srcPath[MAX_PATH]; + + do_normalize(L, srcPath, src); + do_normalize(L, dstPath, dst); + do_translate(dstPath, '\\'); + do_translate(srcPath, '\\'); + + // Promote to wide path + wchar_t wSrcPath[MAX_PATH]; + wchar_t wDstPath[MAX_PATH]; + + MultiByteToWideChar(CP_UTF8, 0, srcPath, -1, wSrcPath, MAX_PATH); + MultiByteToWideChar(CP_UTF8, 0, dstPath, -1, wDstPath, MAX_PATH); + + // If the source path is relative, prepend the current working directory + if (!do_isabsolute(src)) + { + // Get the current working directory + wchar_t cwd[MAX_PATH]; + GetCurrentDirectoryW(MAX_PATH, cwd); + + // Convert the source path to a relative path + wchar_t relSrcPath[MAX_PATH]; + swprintf(relSrcPath, MAX_PATH, L"%c:%s", cwd[0], wSrcPath); + + BOOLEAN res = CreateSymbolicLinkW(wDstPath, relSrcPath, SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE); + return res != 0; + } + else + { + BOOLEAN res = CreateSymbolicLinkW(wDstPath, wSrcPath, SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE); + return res != 0; + } +#else + int res = symlink(src, dst); + return res == 0; +#endif +} + +int os_linkfile(lua_State* L) +{ + const char* src = luaL_checkstring(L, 1); + const char* dst = luaL_checkstring(L, 2); + + int result = do_linkfile(L, src, dst); + if (!result) + { + lua_pushnil(L); + lua_pushfstring(L, "Unable to create link from '%s' to '%s'", src, dst); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} diff --git a/src/host/path_translate.c b/src/host/path_translate.c index c5456abdde..54594fe6e0 100644 --- a/src/host/path_translate.c +++ b/src/host/path_translate.c @@ -18,7 +18,6 @@ void do_translate(char* value, const char sep) } } - static void translate(char* result, const char* value, const char sep) { strcpy(result, value); diff --git a/src/host/premake.c b/src/host/premake.c index 91369fe131..1da4830f7e 100644 --- a/src/host/premake.c +++ b/src/host/premake.c @@ -76,6 +76,8 @@ static const luaL_Reg os_functions[] = { { "hostarch", os_hostarch }, { "isfile", os_isfile }, { "islink", os_islink }, + { "linkdir", os_linkdir }, + { "linkfile", os_linkfile }, { "locate", os_locate }, { "matchdone", os_matchdone }, { "matchisfile", os_matchisfile }, diff --git a/src/host/premake.h b/src/host/premake.h index a5aa0ed15f..9fd3298fbb 100644 --- a/src/host/premake.h +++ b/src/host/premake.h @@ -145,6 +145,8 @@ int os_is64bit(lua_State* L); int os_isdir(lua_State* L); int os_isfile(lua_State* L); int os_islink(lua_State* L); +int os_linkdir(lua_State* L); +int os_linkfile(lua_State* L); int os_locate(lua_State* L); int os_matchdone(lua_State* L); int os_matchisfile(lua_State* L); diff --git a/tests/base/test_os.lua b/tests/base/test_os.lua index b9d639c714..1c0a50e844 100644 --- a/tests/base/test_os.lua +++ b/tests/base/test_os.lua @@ -62,6 +62,22 @@ test.isfalse(os.isfile("no_such_file.lua")) end +-- +-- os.linkdir() and os.linkfile() tests +-- + + function suite.linkdir() + test.istrue(os.linkdir("folder/subfolder", "folder/subfolder2")) + test.istrue(os.islink("folder/subfolder2")) + os.rmdir("folder/subfolder2") + end + + function suite.linkfile() + test.istrue(os.linkfile("folder/ok.lua", "folder/ok2.lua")) + test.istrue(os.islink("folder/ok2.lua")) + os.remove("folder/ok2.lua") + end + -- @@ -274,6 +290,24 @@ end -- +-- os.translateCommand() LINKDIR/LINKFILE tests +-- + function suite.translateCommand_windowsLinkDir() + test.isequal('mklink /d a b', os.translateCommands('{LINKDIR} a b', "windows")) + end + + function suite.translateCommand_windowsLinkFile() + test.isequal('mklink a b', os.translateCommands('{LINKFILE} a b', "windows")) + end + + function suite.translateCommand_posixLinkDir() + test.isequal('ln -s a b', os.translateCommands('{LINKDIR} a b', "posix")) + end + + function suite.translateCommand_posixLinkFile() + test.isequal('ln -s a b', os.translateCommands('{LINKFILE} a b', "posix")) + end +-- -- os.getWindowsRegistry windows tests -- function suite.getreg_nonExistentValue() diff --git a/website/docs/Tokens.md b/website/docs/Tokens.md index 8dce581eaf..b71de6a5bf 100644 --- a/website/docs/Tokens.md +++ b/website/docs/Tokens.md @@ -106,6 +106,8 @@ The available tokens, and their replacements: | {COPYDIR} | xcopy /Q /E /Y /I {args} | cp -rf {args} | | {DELETE} | del {args} | rm -rf {args} | | {ECHO} | echo {args} | echo {args} | +| {LINKDIR} | mklink /d {args} | ln -s {args} | +| {LINKFILE} | mklink {args} | ln -s {args} | | {MKDIR} | IF NOT EXIST {args} (mkdir {args}) | mkdir -p {args} | | {MOVE} | move /Y {args} | mv -f {args} | | {RMDIR} | rmdir /S /Q {args} | rm -rf {args} | @@ -133,6 +135,10 @@ buildcommands { } ``` +### Symbolic Links and Windows + +For Windows, it is required to create symbolic links from an elevated context or to have Developer Mode enabled. The minimum required Windows version to execute symbolic links is Windows 10. + ## Tokens and Filters Tokens are not expanded in filters. See [issue 1306](https://github.com/premake/premake-core/issues/1036#issuecomment-379685035) for some illustrative examples. diff --git a/website/docs/os/os.linkdir.md b/website/docs/os/os.linkdir.md new file mode 100644 index 0000000000..6b36bdfdb0 --- /dev/null +++ b/website/docs/os/os.linkdir.md @@ -0,0 +1,18 @@ +Creates a new symbolic link to a directory. + +```lua +os.linkdir("src", "dst") +``` + +### Parameters ### + +`src` is the path of the directory to create a symbolic link to. +`dst` is the path to the created symbolic link. + +### Return Value ### + +True if successful, otherwise nil and an error message. + +### Availability ### + +Premake 5.0-beta4 or later. \ No newline at end of file diff --git a/website/docs/os/os.linkfile.md b/website/docs/os/os.linkfile.md new file mode 100644 index 0000000000..4a7afa0d22 --- /dev/null +++ b/website/docs/os/os.linkfile.md @@ -0,0 +1,18 @@ +Creates a new symbolic link to a file. + +```lua +os.linkfile("src", "dst") +``` + +### Parameters ### + +`src` is the path of the file to create a symbolic link to. +`dst` is the path to the created symbolic link. + +### Return Value ### + +True if successful, otherwise nil and an error message. + +### Availability ### + +Premake 5.0-beta4 or later. \ No newline at end of file diff --git a/website/sidebars.js b/website/sidebars.js index 1f17c16959..a69e7da788 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -379,6 +379,8 @@ module.exports = { 'os/os.isfile', 'os/os.islink', 'os/os.istarget', + 'os/os.linkdir', + 'os/os.linkfile', 'os/os.locate', 'os/os.matchdirs', 'os/os.matchfiles',