From 8068c4375b7ca6b7e65cd04175ca2b176dfd0f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C4=81vis=20Mos=C4=81ns?= Date: Thu, 5 Mar 2026 21:45:00 +0200 Subject: [PATCH] Fix output folder for absolute path inputs Currently this fails: ``` $ dstep -o /tmp /usr/include/stdio.h /usr/include/stdlib.h dstep: an unknown error occurred: std.file.FileException@std/file.d(840): /usr/include/stdio.d: Permission denied ``` This is because it tries to create .d files in same directory as input file without respecting output folder. This commit fixes this issue so that it will correctly use output folder for such case. --- dstep/driver/Application.d | 21 +++++-- dstep/driver/Util.d | 86 +++++++++++++++++++++++++++++ tests/functional/Tests.d | 39 +++++++++++++ tests/functional/graph/subfile1.d | 3 + tests/functional/include/subfile3.d | 3 + tests/support/DStepRunner.d | 60 +++++++++++++------- 6 files changed, 187 insertions(+), 25 deletions(-) create mode 100644 tests/functional/graph/subfile1.d create mode 100644 tests/functional/include/subfile3.d diff --git a/dstep/driver/Application.d b/dstep/driver/Application.d index da1c98f7..e9a8d997 100644 --- a/dstep/driver/Application.d +++ b/dstep/driver/Application.d @@ -115,8 +115,9 @@ class Application import std.algorithm; import std.array; import std.range; + import std.path; - import dstep.driver.Util : makeDefaultOutputFile; + import dstep.driver.Util : makeDefaultOutputFile, findBasePath; auto inputFiles = config.inputFiles; @@ -130,11 +131,19 @@ class Application } else { - alias fmap = file => Path.buildPath( - config.output, - makeDefaultOutputFile(file, false)); - - return inputFiles.map!fmap.array; + if (config.output.empty) + { + alias fmap = file => makeDefaultOutputFile(file, false); + return inputFiles.map!fmap.array; + } + else + { + auto basePath = findBasePath(inputFiles); + alias fmap = file => Path.buildPath( + config.output, + makeDefaultOutputFile(relativePath(file, basePath), false)); + return inputFiles.map!fmap.array; + } } } diff --git a/dstep/driver/Util.d b/dstep/driver/Util.d index 90e4d3d6..eceaa693 100644 --- a/dstep/driver/Util.d +++ b/dstep/driver/Util.d @@ -15,3 +15,89 @@ string makeDefaultOutputFile(string inputFile, bool useBaseName = true) return setExtension(inputFile, "d"); } + +string findBasePath(string[] paths) +{ + import std.algorithm.iteration : filter, map, reduce; + import std.algorithm.searching : commonPrefix; + import std.array : array; + import std.path : isAbsolute, dirName, pathSplitter, buildPath; + + auto absolutePaths = paths.filter!isAbsolute.array; + if (absolutePaths.length == 0) + return "."; + + version (Windows) + { + import std.string : replace; + absolutePaths = absolutePaths.map!(p => p.replace("/", "\\")).array; + } + + if (absolutePaths.length == 1) + return dirName(absolutePaths[0]); + + auto parts = absolutePaths + .map!(p => pathSplitter(dirName(p)).array) + .array; + + version (Windows) + { + import std.uni : icmp; + auto common = parts.reduce!((a, b) => commonPrefix!((x, y) => icmp(x, y) == 0)(a, b).array); + } + else + { + auto common = parts.reduce!((a, b) => commonPrefix(a, b).array); + } + + if (common.length == 0) + return "."; + + return buildPath(common); +} + +unittest +{ + assert(findBasePath([]) == "."); + assert(findBasePath(["foo/bar.h", "a/b.h", "c.h"]) == "."); +} + +version (Posix) +{ + unittest + { + assert(findBasePath(["/usr/include/stdio.h"]) == "/usr/include"); + assert(findBasePath(["foo.h", "/usr/include/sys/stat.h"]) == "/usr/include/sys"); + assert(findBasePath(["/usr/include/stdio.h", "/usr/include/sys/stat.h", "/usr/lib/foo.h"]) == "/usr"); + assert(findBasePath(["/usr/include/stdio.h", "/usr/include/string.h", "/usr/include/stdlib.h"]) == "/usr/include"); + assert(findBasePath(["usr.h", "/usr/include/stdio.h", "/usr/include/sys/stat.h", "/usr/include/string.h", "include.h"]) == "/usr/include"); + assert(findBasePath(["/usr/include/stdio.h", "/usr/Include/stdio.h", "/usR/include/stdio.h"]) == "/"); + } +} + +version (Windows) +{ + unittest + { + assert(findBasePath(["/usr/include/stdio.h"]) == "."); + assert(findBasePath(["foo.h", "/usr/include/sys/stat.h"]) == "."); + assert(findBasePath(["/usr/include/stdio.h", "/usr/include/sys/stat.h", "/usr/lib/foo.h"]) == "."); + assert(findBasePath(["/usr/include/stdio.h", "/usr/include/string.h", "/usr/include/stdlib.h"]) == "."); + assert(findBasePath(["usr.h", "/usr/include/stdio.h", "/usr/include/sys/stat.h", "/usr/include/string.h", "include.h"]) == "."); + assert(findBasePath(["/usr/include/stdio.h", "/usr/Include/stdio.h", "/usR/include/stdio.h"]) == "."); + + assert(findBasePath(["C:/usr/include/stdio.h"]) == "C:\\usr\\include"); + assert(findBasePath(["foo.h", "C:/usr/include/sys/stat.h"]) == "C:\\usr\\include\\sys"); + assert(findBasePath(["C:/usr/include/stdio.h", "C:/usr/include/sys/stat.h", "C:/usr/lib/foo.h"]) == "C:\\usr"); + assert(findBasePath(["C:/usr/include/stdio.h", "C:/usr/include/string.h", "C:/usr/include/stdlib.h"]) == "C:\\usr\\include"); + assert(findBasePath(["usr.h", "D:/usr/include/stdio.h", "D:/usr/include/sys/stat.h", "D:/usr/include/string.h", "include.h"]) == "D:\\usr\\include"); + assert(findBasePath(["E:/usr/include/stdio.h", "E:/usr/Include/stdio.h", "E:/usR/include/stdio.h"]) == "E:\\usr\\include"); + + assert(findBasePath(["D:\\test.h"]) == "D:\\"); + assert(findBasePath(["D:\\a\\b.h", "D:\\a\\c.h"]) == "D:\\a"); + assert(findBasePath(["D:\\a\\b.h", "E:\\b.h"]) == "."); + assert(findBasePath(["D:\\B\\a.h", "D:\\b\\c\\D.h", "D:\\B\\c.h"]) == "D:\\B"); + assert(findBasePath(["D:/a\\b/d.h", "D:\\a/B\\c.h"]) == "D:\\a\\b"); + assert(findBasePath(["C:\\a.h", "D:\\b.h"]) == "."); + } +} diff --git a/tests/functional/Tests.d b/tests/functional/Tests.d index 7eebf707..9c255403 100644 --- a/tests/functional/Tests.d +++ b/tests/functional/Tests.d @@ -9,6 +9,7 @@ import tests.support.Assertions; import std.algorithm : canFind; import std.process : executeShell; import std.typecons; +import std.path : absolutePath; void printClangVersion() { @@ -294,6 +295,44 @@ unittest assertIssuesWarning("tests/functional/collision.h"); } +// DStep should output to correct directory when using absolute paths. +unittest +{ + assertRunsDStepCFiles([ + TestFile("tests/functional/graph/subfile1.d", absolutePath("tests/functional/graph/subfile1.h")), + TestFile("tests/functional/include/subfile3.d", absolutePath("tests/functional/include/subfile3.h"))] + ); +} + +unittest +{ + assertRunsDStepCFile( + "tests/functional/functions.d", + absolutePath("tests/functional/functions.h") + ); +} + +// Output in same directory when single file given and no output specified +unittest +{ + assertRunsDStepCFiles( + [TestFile("tests/functional/functions.d", absolutePath("tests/functional/functions.h"))], + ["--unspecified-output"], + false); +} + +// Output in same directory when using multiple files with no output folder specified +unittest +{ + assertRunsDStepCFiles( + [TestFile("tests/functional/aggregate.d", "tests/functional/aggregate.h"), + TestFile("tests/functional/functions.d", "tests/functional/functions.h"), + TestFile("tests/functional/graph/subfile1.d", absolutePath("tests/functional/graph/subfile1.h")), + TestFile("tests/functional/include/subfile3.d", absolutePath("tests/functional/include/subfile3.h"))], + ["--unspecified-output"] + ); +} + version (OSX): // Objective-C tests diff --git a/tests/functional/graph/subfile1.d b/tests/functional/graph/subfile1.d new file mode 100644 index 00000000..673ce4f1 --- /dev/null +++ b/tests/functional/graph/subfile1.d @@ -0,0 +1,3 @@ +extern (C): + +extern __gshared int content; diff --git a/tests/functional/include/subfile3.d b/tests/functional/include/subfile3.d new file mode 100644 index 00000000..ad13ba87 --- /dev/null +++ b/tests/functional/include/subfile3.d @@ -0,0 +1,3 @@ +extern (C): + +void core (int); diff --git a/tests/support/DStepRunner.d b/tests/support/DStepRunner.d index 2974a408..f73d2037 100644 --- a/tests/support/DStepRunner.d +++ b/tests/support/DStepRunner.d @@ -21,13 +21,13 @@ auto testRunDStep( { import core.exception : AssertError; - import std.algorithm : canFind, map; - import std.file : exists, isFile, readText, rmdirRecurse; - import std.path : buildPath; + import std.algorithm : canFind, map, remove; + import std.file : exists, isFile, readText, mkdirRecurse, rmdirRecurse, copy, getcwd; + import std.path : buildPath, isAbsolute, relativePath, dirName; import std.range : join; - import std.array : empty; + import std.array : empty, array; - import dstep.driver.Util : makeDefaultOutputFile; + import dstep.driver.Util : makeDefaultOutputFile, findBasePath; version (OptionalGNUStep) { @@ -48,31 +48,53 @@ auto testRunDStep( string outputDir = namedTempDir("dstepUnitTest"); scope(exit) rmdirRecurse(outputDir); + auto argumentsLength = arguments.length; + arguments = arguments.remove!(x => x == "--unspecified-output"); + bool unspecifiedOutput = arguments.length < argumentsLength; + string[] outputPaths; + string workDir = getcwd(); - if (sourcePaths.length == 1) - { - outputPaths ~= buildPath(outputDir, - makeDefaultOutputFile(sourcePaths[0], false)); - } - else + auto sourceBasePath = findBasePath(sourcePaths); + foreach (ref sourcePath; sourcePaths) { - foreach (sourcePath; sourcePaths) - outputPaths ~= buildPath(outputDir, - makeDefaultOutputFile(sourcePath, false)); + string path = sourcePath; + if (isAbsolute(sourcePath)) + { + path = relativePath(sourcePath, unspecifiedOutput ? workDir : sourceBasePath); + } + if (unspecifiedOutput) + { + auto updatedSourcePath = buildPath(outputDir, path); + mkdirRecurse(dirName(updatedSourcePath)); + copy(sourcePath, updatedSourcePath); + if (isAbsolute(sourcePath)) + { + sourcePath = updatedSourcePath; + } + } + outputPaths ~= buildPath(outputDir, makeDefaultOutputFile(path, false)); } - auto localCommand = ["./bin/dstep"] ~ sourcePaths ~ arguments; + auto dstepPath = buildPath(workDir, "bin", "dstep"); + auto localCommand = [dstepPath] ~ sourcePaths ~ arguments; - if (outputPaths.length == 1) - localCommand ~= ["-o", outputPaths[0]]; + if (unspecifiedOutput) + { + workDir = outputDir; + } else - localCommand ~= ["-o", outputDir]; + { + if (outputPaths.length == 1) + localCommand ~= ["-o", outputPaths[0]]; + else + localCommand ~= ["-o", outputDir]; + } if (command) *command = join(localCommand, " "); - auto result = execute(localCommand); + auto result = execute(localCommand, workDir: workDir); if (outputContents) outputContents.length = outputPaths.length;