diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs index e600d8e0..2d7b5f9c 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs @@ -127,6 +127,20 @@ public PInvokeGeneratorConfiguration(string language, string languageStandard, s { throw new ArgumentOutOfRangeException(nameof(options)); } + else if ((options & PInvokeGeneratorConfigurationOptions.GenerateMultipleFiles) == 0 && Directory.Exists(_outputLocation)) + { + // In single-file mode the output location is treated as a file path; pointing it at an + // existing directory otherwise surfaces as a confusing UnauthorizedAccessException (or a + // DirectoryNotFoundException with a trailing separator) when the stream is opened. + throw new ArgumentException($"The output location '{_outputLocation}' is an existing directory, but single-file output was requested. Specify a file path for the output location, or pass '--config multi-file' to generate multiple files into the directory.", nameof(outputLocation)); + } + else if ((options & PInvokeGeneratorConfigurationOptions.GenerateMultipleFiles) != 0 && File.Exists(_outputLocation)) + { + // In multi-file mode the output location is treated as a directory that each file is + // written into; pointing it at an existing file otherwise fails when that directory is + // created. + throw new ArgumentException($"The output location '{_outputLocation}' is an existing file, but multi-file output was requested. Specify a directory path for the output location, or remove '--config multi-file' to generate a single file.", nameof(outputLocation)); + } if ((options & PInvokeGeneratorConfigurationOptions.GeneratePreviewCode) != 0) { diff --git a/sources/ClangSharpPInvokeGenerator/Program.cs b/sources/ClangSharpPInvokeGenerator/Program.cs index e9f2dd4a..39e49066 100644 --- a/sources/ClangSharpPInvokeGenerator/Program.cs +++ b/sources/ClangSharpPInvokeGenerator/Program.cs @@ -537,35 +537,46 @@ public static int Run() translationFlags |= CXTranslationUnit_IncludeAttributedTypes; // Include attributed types in CXType translationFlags |= CXTranslationUnit_VisitImplicitAttributes; // Implicit attributes should be visited - var config = new PInvokeGeneratorConfiguration(language, std, namespaceName, outputLocation, headerFile, outputMode, configOptions) { - DefaultClass = methodClassName, - ExcludedNames = excludedNames, - IncludedNames = includedNames, - LibraryPath = libraryPath, - MethodPrefixToStrip = methodPrefixToStrip, - NativeTypeNamesToStrip = nativeTypeNamesToStrip, - RemappedNames = remappedNames, - RemappedTypeNames = remappedTypeNames, - RemappedFieldNames = remappedFieldNames, - TraversalNames = traversalNames, - TestOutputLocation = testOutputLocation, - WithAccessSpecifiers = withAccessSpecifiers, - WithAttributes = withAttributes, - WithCallConvs = withCallConvs, - WithClasses = withClasses, - WithGuids = withGuids, - WithLengths = withLengths, - WithLibraryPaths = withLibraryPaths, - WithManualImports = withManualImports, - WithNamespaces = withNamespaces, - WithReadonlys = withReadonlys, - WithSetLastErrors = withSetLastErrors, - WithSuppressGCTransitions = withSuppressGCTransitions, - WithTransparentStructs = withTransparentStructs, - WithTypes = withTypes, - WithUsings = withUsings, - WithPackings = withPackings, - }; + PInvokeGeneratorConfiguration config; + + try + { + config = new PInvokeGeneratorConfiguration(language, std, namespaceName, outputLocation, headerFile, outputMode, configOptions) { + DefaultClass = methodClassName, + ExcludedNames = excludedNames, + IncludedNames = includedNames, + LibraryPath = libraryPath, + MethodPrefixToStrip = methodPrefixToStrip, + NativeTypeNamesToStrip = nativeTypeNamesToStrip, + RemappedNames = remappedNames, + RemappedTypeNames = remappedTypeNames, + RemappedFieldNames = remappedFieldNames, + TraversalNames = traversalNames, + TestOutputLocation = testOutputLocation, + WithAccessSpecifiers = withAccessSpecifiers, + WithAttributes = withAttributes, + WithCallConvs = withCallConvs, + WithClasses = withClasses, + WithGuids = withGuids, + WithLengths = withLengths, + WithLibraryPaths = withLibraryPaths, + WithManualImports = withManualImports, + WithNamespaces = withNamespaces, + WithReadonlys = withReadonlys, + WithSetLastErrors = withSetLastErrors, + WithSuppressGCTransitions = withSuppressGCTransitions, + WithTransparentStructs = withTransparentStructs, + WithTypes = withTypes, + WithUsings = withUsings, + WithPackings = withPackings, + }; + } + catch (ArgumentException e) + { + Console.Error.Write($"Error: {e.Message}"); + Console.Error.Write(Environment.NewLine); + return -1; + } if (config.GenerateMacroBindings) { diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/ConfigurationOutputLocationTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/ConfigurationOutputLocationTest.cs new file mode 100644 index 00000000..2a4b6805 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/ConfigurationOutputLocationTest.cs @@ -0,0 +1,87 @@ +// Copyright (c) .NET Foundation and Contributors. All Rights Reserved. Licensed under the MIT License (MIT). See License.md in the repository root for more information. + +using System.IO; +using NUnit.Framework; + +namespace ClangSharp.UnitTests; + +public sealed class ConfigurationOutputLocationTest +{ + private static PInvokeGeneratorConfiguration CreateConfiguration(string outputLocation, PInvokeGeneratorConfigurationOptions options) + { + return new PInvokeGeneratorConfiguration("c++", "c++17", "ClangSharp.Test", outputLocation, headerFile: null, PInvokeGeneratorOutputMode.CSharp, options); + } + + [Test] + public void SingleFileModeRejectsExistingDirectory() + { + var directory = Directory.CreateTempSubdirectory("ClangSharpTest"); + + try + { + var exception = Assert.Throws(() => CreateConfiguration(directory.FullName, PInvokeGeneratorConfigurationOptions.None))!; + Assert.That(exception.Message, Does.Contain("existing directory")); + Assert.That(exception.Message, Does.Contain("--config multi-file")); + } + finally + { + directory.Delete(recursive: true); + } + } + + [Test] + public void MultiFileModeRejectsExistingFile() + { + var file = Path.GetTempFileName(); + + try + { + var exception = Assert.Throws(() => CreateConfiguration(file, PInvokeGeneratorConfigurationOptions.GenerateMultipleFiles))!; + Assert.That(exception.Message, Does.Contain("existing file")); + Assert.That(exception.Message, Does.Contain("--config multi-file")); + } + finally + { + File.Delete(file); + } + } + + [Test] + public void SingleFileModeAllowsExistingFile() + { + var file = Path.GetTempFileName(); + + try + { + Assert.DoesNotThrow(() => CreateConfiguration(file, PInvokeGeneratorConfigurationOptions.None)); + } + finally + { + File.Delete(file); + } + } + + [Test] + public void MultiFileModeAllowsExistingDirectory() + { + var directory = Directory.CreateTempSubdirectory("ClangSharpTest"); + + try + { + Assert.DoesNotThrow(() => CreateConfiguration(directory.FullName, PInvokeGeneratorConfigurationOptions.GenerateMultipleFiles)); + } + finally + { + directory.Delete(recursive: true); + } + } + + [Test] + public void NonExistentOutputLocationIsAllowedInEitherMode() + { + var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + Assert.DoesNotThrow(() => CreateConfiguration(path, PInvokeGeneratorConfigurationOptions.None)); + Assert.DoesNotThrow(() => CreateConfiguration(path, PInvokeGeneratorConfigurationOptions.GenerateMultipleFiles)); + } +}