diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6667feadfe7e..6aaace1b2648 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -22,9 +22,10 @@ Testing: - Examples: - `dotnet test test/dotnet.Tests/dotnet.Tests.csproj --filter "Name~ItShowsTheAppropriateMessageToTheUser"` - `dotnet exec artifacts/bin/redist/Debug/dotnet.Tests.dll -method "*ItShowsTheAppropriateMessageToTheUser*"` +- For incremental test runs of `dotnet.Tests` (avoids slow full `build.cmd`), use the `incremental-test` skill. - To test CLI command changes: - Build the redist SDK: `./build.sh` from repo root - - Create a dogfood environment: `source eng/dogfood.sh` + - Create a dogfood environment: `source eng/dogfood.sh` - Test commands in the dogfood shell (e.g., `dnx --help`, `dotnet tool install --help`) - The dogfood script sets up PATH and environment to use the newly built SDK diff --git a/.github/skills b/.github/skills deleted file mode 120000 index 9af1c1454ae3..000000000000 --- a/.github/skills +++ /dev/null @@ -1 +0,0 @@ -D:/code/dotnet-sdk/.claude/skills/ \ No newline at end of file diff --git a/.github/skills/AGENTS.md b/.github/skills/AGENTS.md new file mode 100644 index 000000000000..0b022c9eba26 --- /dev/null +++ b/.github/skills/AGENTS.md @@ -0,0 +1,24 @@ +# Agent Skills + +When creating skills, follow: +- Agent skills specification: https://agentskills.io/specification.md +- Best practices: https://agentskills.io/skill-creation/best-practices.md + +## Structure + +``` +.github/skills/skill-name/ +├── SKILL.md # Required: metadata + instructions +├── scripts/ # Optional: executable code +├── references/ # Optional: documentation +├── assets/ # Optional: templates, resources +└── ... # Any additional files or directories +``` + +## Quick Checklist + +- [ ] Run `dotnet .github/skills/ValidateSkill.cs ` to validate format. +- [ ] `description` describes what the skill does and when to use it. Skill body does not include "When to use this skill". +- [ ] Skill does not explain things the agent already knows. Focus on what's specific to the task at hand. +- [ ] Deterministic processes use scripts (for example, to fetch and format data from an API). +- [ ] Scripts use PowerShell or .NET file-based apps, not bash. diff --git a/.github/skills/ValidateSkill.cs b/.github/skills/ValidateSkill.cs new file mode 100755 index 000000000000..12d1e0b51342 --- /dev/null +++ b/.github/skills/ValidateSkill.cs @@ -0,0 +1,103 @@ +#!/usr/bin/env dotnet +#:property ManagePackageVersionsCentrally=false +#:property PublishAot=false +#:package YamlDotNet@16.3.0 + +using YamlDotNet.Serialization; +using System.Text.RegularExpressions; + +if (args.Length == 0) +{ + Console.Error.WriteLine("Usage: dotnet ValidateSkill.cs "); + return 1; +} + +string skillDir = Path.GetFullPath(args[0]); +string skillName = Path.GetFileName(Path.TrimEndingDirectorySeparator(skillDir)); +string skillFile = Path.Combine(skillDir, "SKILL.md"); + +// SKILL.md must exist in the skill directory +if (!File.Exists(skillFile)) +{ + Console.Error.WriteLine($"SKILL.md not found in {skillDir}"); + return 1; +} + +string text = File.ReadAllText(skillFile); + +// SKILL.md must begin with YAML frontmatter delimited by --- +if (!text.StartsWith("---")) +{ + Console.Error.WriteLine("No YAML frontmatter found."); + return 1; +} + +Match frontmatterMatch = Regex.Match( + text, + @"\A---\r?\n(?.*?)(?:\r?\n)---(?:\r?\n|$)", + RegexOptions.Singleline); +if (!frontmatterMatch.Success) +{ + Console.Error.WriteLine("Unterminated YAML frontmatter."); + return 1; +} + +string yaml = frontmatterMatch.Groups["yaml"].Value.Trim(); + +IDeserializer deserializer = new DeserializerBuilder().Build(); +Dictionary frontmatter = deserializer.Deserialize>(yaml); + +// name is required +if (!frontmatter.TryGetValue("name", out object? nameValue) || nameValue is not string frontmatterName) +{ + Console.Error.WriteLine("Frontmatter missing 'name' field."); + return 1; +} + +// name must be 1-64 characters +if (frontmatterName.Length == 0 || frontmatterName.Length > 64) +{ + Console.Error.WriteLine($"Name is {frontmatterName.Length} chars (must be 1-64)."); + return 1; +} + +// name: lowercase alphanumeric and hyphens only, no leading/trailing/consecutive hyphens +if (!Regex.IsMatch(frontmatterName, @"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$") + || frontmatterName.Contains("--")) +{ + Console.Error.WriteLine($"Invalid name '{frontmatterName}'. Must be lowercase letters, numbers, and hyphens only. Must not start/end with a hyphen or contain consecutive hyphens."); + return 1; +} + +// name must match the parent directory name +if (!string.Equals(skillName, frontmatterName, StringComparison.Ordinal)) +{ + Console.Error.WriteLine($"Name mismatch: directory is '{skillName}' but SKILL.md name is '{frontmatterName}'."); + return 1; +} + +// description is required +if (!frontmatter.TryGetValue("description", out object? descValue) || descValue is not string description) +{ + Console.Error.WriteLine("Frontmatter missing 'description' field."); + return 1; +} + +// description must be 1-1024 characters +if (description.Length == 0 || description.Length > 1024) +{ + Console.Error.WriteLine($"Description is {description.Length} chars (must be 1-1024)."); + return 1; +} + +// Keep SKILL.md under 500 lines; move detailed content to references/ or scripts/ +// See "Progressive Disclosure" at https://agentskills.io/specification.md +int lineCount = text.Split('\n').Length; +if (lineCount > 500) +{ + Console.Error.WriteLine($"SKILL.md is {lineCount} lines (max 500). See \"Progressive Disclosure\" at https://agentskills.io/specification.md"); + return 1; +} + +Console.WriteLine($"Skill '{frontmatterName}' is valid."); +return 0; diff --git a/.github/skills/incremental-test/SKILL.md b/.github/skills/incremental-test/SKILL.md new file mode 100644 index 000000000000..f4d927873152 --- /dev/null +++ b/.github/skills/incremental-test/SKILL.md @@ -0,0 +1,106 @@ +--- +name: incremental-test +description: >- + Run dotnet.Tests incrementally without a full build.cmd rebuild. Use after + modifying source code in SDK projects to quickly build only changed projects, + deploy their outputs into the redist SDK layout, and run tests against them. +--- + +# Incremental Test Runner for dotnet.Tests + +## Prerequisites + +- A full build must have been completed at least once (via `build.cmd` or `build.sh`) so that the redist SDK layout exists at `artifacts\bin\redist\Debug\dotnet\sdk\\`. +- The repo-local `.dotnet` SDK must match the version expected by the test projects. If the runtime or SDK version is out of date (e.g., test build fails with a missing framework error), run `.\restore.cmd` (or `./restore.sh` on macOS/Linux) to download the correct SDK into `.dotnet`. +- This workflow uses Windows/PowerShell commands and paths. On macOS/Linux, substitute forward slashes and use `cp` instead of `Copy-Item`. + +## Workflow + +### Step 1: Identify modified projects + +Determine which projects have been modified. Use context from: +- The files you just edited in this session. +- Or `git status`/`git diff` to find changed `.cs` files and map them to their `.csproj` projects. + +### Step 2: Build modified projects + +Build each modified project individually using the repo-local dotnet: + +``` +.\.dotnet\dotnet build -c Debug +``` + +For example: +``` +.\.dotnet\dotnet build src\Cli\Microsoft.DotNet.Cli.Utils\Microsoft.DotNet.Cli.Utils.csproj -c Debug +``` + +If the `dotnet` CLI project itself was modified, build it: +``` +.\.dotnet\dotnet build src\Cli\dotnet\dotnet.csproj -c Debug +``` + +### Step 3: Copy output DLLs to the redist SDK layout + +Discover the SDK version directory name: +```powershell +$sdkVersion = (Get-ChildItem artifacts\bin\redist\Debug\dotnet\sdk -Directory | Sort-Object LastWriteTime -Descending | Select-Object -First 1).Name +``` + +For each modified project, copy its output DLL (and any satellite assemblies) from the project's build output to the redist SDK directory: + +``` +Source: artifacts\bin\\Debug\net10.0\.dll +Target: artifacts\bin\redist\Debug\dotnet\sdk\\ +``` + +For example: +```powershell +Copy-Item artifacts\bin\Microsoft.DotNet.ProjectTools\Debug\net10.0\Microsoft.DotNet.ProjectTools.dll artifacts\bin\redist\Debug\dotnet\sdk\$sdkVersion\ +Copy-Item artifacts\bin\Microsoft.DotNet.Cli.Utils\Debug\net10.0\Microsoft.DotNet.Cli.Utils.dll artifacts\bin\redist\Debug\dotnet\sdk\$sdkVersion\ +``` + +The `dotnet` project is special — it builds into `artifacts\bin\dotnet\Debug\net10.0\` and its `dotnet.dll` must be copied to the SDK directory: +```powershell +Copy-Item artifacts\bin\dotnet\Debug\net10.0\dotnet.dll artifacts\bin\redist\Debug\dotnet\sdk\$sdkVersion\ +``` + +**Important notes:** +- For typical incremental edits, only copy DLLs that are **already present** in the target directory. If your change introduces a new shipped assembly or moves assemblies, you will need a full `build.cmd`/`build.sh` to update the layout correctly. +- Some projects multi-target (e.g., `net10.0` and `net472`). Always use the `net10.0` output. +- If localization resource DLLs were changed (in subdirectories like `cs\`, `de\`, etc.), copy those too. + +### Step 4: Build the test project (if test code was modified) + +The test project `test\dotnet.Tests\dotnet.Tests.csproj` outputs directly to `artifacts\bin\redist\Debug\` (via `TestHostFolder`), so just build it: + +``` +.\.dotnet\dotnet build test\dotnet.Tests\dotnet.Tests.csproj +``` + +### Step 5: Run the tests + +Run specific tests: +``` +.\.dotnet\dotnet exec artifacts\bin\redist\Debug\dotnet.Tests.dll -method "*TestMethodName*" +``` + +Or run filtered tests via `dotnet test`: +``` +.\.dotnet\dotnet test test\dotnet.Tests\dotnet.Tests.csproj --no-build --filter "Name~TestMethodName" +``` + +## Common project paths + +| Assembly | Project Path | +|---|---| +| `dotnet.dll` | `src\Cli\dotnet\dotnet.csproj` | +| `Microsoft.DotNet.Cli.Utils.dll` | `src\Cli\Microsoft.DotNet.Cli.Utils\Microsoft.DotNet.Cli.Utils.csproj` | +| `Microsoft.DotNet.Cli.Definitions.dll` | `src\Cli\Microsoft.DotNet.Cli.Definitions\Microsoft.DotNet.Cli.Definitions.csproj` | +| `Microsoft.DotNet.Cli.CoreUtils.dll` | `src\Cli\Microsoft.DotNet.Cli.CoreUtils\Microsoft.DotNet.Cli.CoreUtils.csproj` | +| `Microsoft.DotNet.Configurer.dll` | `src\Cli\Microsoft.DotNet.Configurer\Microsoft.DotNet.Configurer.csproj` | +| `Microsoft.DotNet.ProjectTools.dll` | `src\Microsoft.DotNet.ProjectTools\Microsoft.DotNet.ProjectTools.csproj` | +| `Microsoft.DotNet.NativeWrapper.dll` | `src\Resolvers\Microsoft.DotNet.NativeWrapper\Microsoft.DotNet.NativeWrapper.csproj` | +| `Microsoft.DotNet.TemplateLocator.dll` | `src\Microsoft.DotNet.TemplateLocator\Microsoft.DotNet.TemplateLocator.csproj` | +| `Microsoft.DotNet.InternalAbstractions.dll` | `src\Cli\Microsoft.DotNet.InternalAbstractions\Microsoft.DotNet.InternalAbstractions.csproj` | +| `dotnet.Tests.dll` | `test\dotnet.Tests\dotnet.Tests.csproj` | diff --git a/.github/skills~d4df2c00059096f30eabafe83ba1722cf97d8ecd b/.github/skills~d4df2c00059096f30eabafe83ba1722cf97d8ecd new file mode 100644 index 000000000000..9af1c1454ae3 --- /dev/null +++ b/.github/skills~d4df2c00059096f30eabafe83ba1722cf97d8ecd @@ -0,0 +1 @@ +D:/code/dotnet-sdk/.claude/skills/ \ No newline at end of file diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md index bdaf0176fdaa..9c9ed03ab167 100644 --- a/documentation/general/dotnet-run-file.md +++ b/documentation/general/dotnet-run-file.md @@ -52,6 +52,8 @@ Additionally, the implicit project file has the following customizations: string? directoryPath = AppContext.GetData("EntryPointFileDirectoryPath") as string; ``` + - `EntryPointFilePath` property is set to the entry-point file path and is made visible to analyzers via `CompilerVisibleProperty`. + - `FileBasedProgram` property is set to `true` and can be used by SDK targets to detect file-based apps. - `DisableDefaultItemsInProjectFolder` property is set to `true` which results in `EnableDefaultItems=false` by default @@ -110,8 +112,8 @@ If a dash (`-`) is given instead of the target path (i.e., `dotnet run -`), the In this case, the current working directory is not used to search for other files (launch profiles, other sources in case of multi-file apps); the compilation consists solely of the single file read from the standard input. However, the current working directory is still used as the working directory for building and executing the program. -To reference projects relative to the current working directory (instead of relative to the temporary directory the file is isolated in), -you can use something like `#:project $(MSBuildStartupDirectory)/relative/path`. +To reference projects or files relative to the current working directory (instead of relative to the temporary directory the file is isolated in), +you can use something like `#:project $(MSBuildStartupDirectory)/relative/path` or `#:ref $(MSBuildStartupDirectory)/relative/lib.cs`. `dotnet path.cs` is a shortcut for `dotnet run --file path.cs` provided that `path.cs` is a valid [target path](#target-path) (`dotnet -` is currently not supported) and it is not a DLL path, built-in command, or a NuGet tool (e.g., `dotnet watch` invokes the `dotnet-watch` tool @@ -165,7 +167,7 @@ removes current user's `dotnet run` build outputs that haven't been used in 30 d They are not cleaned immediately because they can be re-used on subsequent runs for better performance. The automatic cleanup can be disabled by environment variable `DOTNET_CLI_DISABLE_FILE_BASED_APP_ARTIFACTS_AUTOMATIC_CLEANUP=true`, but other parameters of the automatic cleanup are currently not configurable. -The same cleanup can be performed manually via command `dotnet clean-file-based-app-artifacts`. +The same cleanup can be performed manually via command `dotnet clean file-based-apps`. ## Directives for project metadata @@ -178,11 +180,12 @@ which are [ignored][ignored-directives] by the C# language but recognized by the #:property LangVersion=preview #:package System.CommandLine@2.0.0-* #:project ../MyLibrary +#:ref ../lib/lib.cs #:include ./**/*.cs ``` Each directive has a kind (e.g., `package`), a name (e.g., `System.CommandLine`), a separator (e.g., `@`), and a value (e.g., the package version). -The value is required for `#:property`, optional for `#:package`/`#:sdk`, and disallowed for `#:project`/`#:include`. +The value is required for `#:property`, optional for `#:package`/`#:sdk`, and disallowed for `#:project`/`#:ref`/`#:include`. The name must be separated from the kind of the directive by whitespace and any leading and trailing white space is not considered part of the name and value. @@ -209,6 +212,26 @@ The directives are processed as follows: (because `ProjectReference` items don't support directory paths). An error is reported if zero or more than one projects are found in the directory, just like `dotnet reference add` would do. +- Each `#:ref` references another `.cs` file as a separate project reference. + A virtual project is created for the referenced file (e.g., `lib.cs` produces a virtual `lib.cs.csproj`), + and a `` is injected in an ``. + It is an error if the name is empty or if the referenced file does not exist. + Unlike `#:project`, `#:ref` points to a `.cs` file (not a `.csproj` file or directory). + + The referenced file is itself a file-based program with its own virtual project (defaulting to `OutputType=Exe`). + Library files without an entry point should use `#:property OutputType=Library` to avoid compilation errors. + Because the referenced file is compiled as a separate assembly, internal members of the referenced file are not accessible from the referencing file. + The `#:ref` directive is transitive: a referenced file can itself contain `#:ref` directives (or any other directives). + + Relative paths are resolved relative to the file containing the directive. + MSBuild variables (like `$(MSBuildProjectDirectory)`) can be used in the path. + + During [conversion](#grow-up), each `#:ref` directive creates a separate library project in a sibling directory + and a corresponding `` entry is added to the converted project. + The conversion is recursive: any `#:ref` directives in the referenced files are also converted in the same way. + + This directive is currently gated under a feature flag that can be enabled by setting the MSBuild property `ExperimentalFileBasedProgramEnableRefDirective=true`. + - Each `#:include` is injected as `<{1} Include="{0}" />` in an `` where `{0}` is the directive's value and `{1}` is determined by its extension. The mapping can be customized by setting the MSBuild property `FileBasedProgramsItemMapping` @@ -228,9 +251,9 @@ The directives are processed as follows: - Other directive kinds result in an error, reserving them for future use. Directive values support MSBuild variables (like `$(..)`) normally as they are translated literally and left to MSBuild engine to process. -However, in `#:project` directives, variables might not be preserved during [grow up](#grow-up), +However, in `#:project` and `#:ref` directives, variables might not be preserved during [grow up](#grow-up), because there is additional processing of those directives that makes it technically challenging to preserve variables in all cases -(project directive values need to be resolved to be relative to the target directory +(project and ref directive values need to be resolved to be relative to the target directory and also to point to a project file rather than a directory). Note that it is not expected that variables inside the path change their meaning during the conversion, so for example `#:project ../$(LibName)` is translated to `` (i.e., the variable is preserved). @@ -270,6 +293,11 @@ Along with `#:`, the language also ignores `#!` which could be then used for [sh Console.WriteLine("Hello"); ``` +When a file-based program uses [`#:include`](#multiple-files) directives to include additional files, +the entry point file should start with `#!` to clearly distinguish it from included files. +This helps IDEs to properly handle multi-file scenarios and discover entry points. +The analyzer **CA2266** reports a warning if the entry point file is missing the shebang line in this scenario. + ## Implementation The build is performed using MSBuild APIs on in-memory project files. diff --git a/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListCommandDefinition.cs index ec63176267cd..cefd8ab10014 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListCommandDefinition.cs @@ -20,7 +20,7 @@ public static Argument CreateSlnOrProjectArgument(string name, string de Arity = ArgumentArity.ZeroOrOne }.DefaultToCurrentDirectory(); - public readonly Argument SlnOrProjectArgument = CreateSlnOrProjectArgument(CommandDefinitionStrings.SolutionOrProjectArgumentName, CommandDefinitionStrings.SolutionOrProjectArgumentDescription); + public readonly Argument SlnOrProjectOrFileArgument = CreateSlnOrProjectArgument(CommandDefinitionStrings.SolutionOrProjectOrFileArgumentName, CommandDefinitionStrings.SolutionOrProjectOrFileArgumentDescription); public readonly ListPackageCommandDefinition PackageCommand = new(); public readonly ListReferenceCommandDefinition ReferenceCommand = new(); @@ -31,7 +31,7 @@ public ListCommandDefinition() Hidden = true; this.DocsLink = Link; - Arguments.Add(SlnOrProjectArgument); + Arguments.Add(SlnOrProjectOrFileArgument); Subcommands.Add(PackageCommand); Subcommands.Add(ReferenceCommand); } diff --git a/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListPackageCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListPackageCommandDefinition.cs index f79679563733..6e9d02b473c8 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListPackageCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListPackageCommandDefinition.cs @@ -12,6 +12,6 @@ internal sealed class ListPackageCommandDefinition() : PackageListCommandDefinit public ListCommandDefinition Parent => (ListCommandDefinition)Parents.Single(); - public override string? GetFileOrDirectory(ParseResult parseResult) - => parseResult.GetValue(Parent.SlnOrProjectArgument); + public override Argument? GetProjectOrFileArgument() + => Parent.SlnOrProjectOrFileArgument; } diff --git a/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListReferenceCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListReferenceCommandDefinition.cs index 748187d7f4fe..d49e703a2d42 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListReferenceCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Hidden/List/ListReferenceCommandDefinition.cs @@ -19,7 +19,7 @@ public ListReferenceCommandDefinition() : base(Name) public ListCommandDefinition Parent => (ListCommandDefinition)Parents.Single(); internal override string? GetFileOrDirectory(ParseResult parseResult) - => parseResult.GetValue(Parent.SlnOrProjectArgument); + => parseResult.GetValue(Parent.SlnOrProjectOrFileArgument); } internal abstract class ListReferenceCommandDefinitionBase : Command diff --git a/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Package/PackageListCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Package/PackageListCommandDefinition.cs index 9e0eb70520c9..2db06fe0fb97 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Package/PackageListCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/Package/PackageListCommandDefinition.cs @@ -6,20 +6,12 @@ namespace Microsoft.DotNet.Cli.Commands.Package.List; -internal sealed class PackageListCommandDefinition : PackageListCommandDefinitionBase +internal sealed class PackageListCommandDefinition() : PackageListCommandDefinitionBase(Name) { public new const string Name = "list"; - public readonly Option ProjectOption = PackageCommandDefinition.CreateProjectOption(); - - public PackageListCommandDefinition() - : base(Name) - { - Options.Add(ProjectOption); - } - - public override string? GetFileOrDirectory(ParseResult parseResult) - => parseResult.GetValue(ProjectOption); + public override Argument? GetProjectOrFileArgument() + => null; } internal abstract class PackageListCommandDefinitionBase : Command @@ -110,6 +102,9 @@ internal abstract class PackageListCommandDefinitionBase : Command Description = CommandDefinitionStrings.CmdOutputVersionDescription }.ForwardAsSingle(o => $"--output-version:{o}"); + public readonly Option ProjectOption = PackageCommandDefinition.CreateProjectOption(); + public readonly Option FileOption = PackageCommandDefinition.CreateFileOption(); + public PackageListCommandDefinitionBase(string name) : base(name, CommandDefinitionStrings.PackageListAppFullName) { @@ -128,9 +123,11 @@ public PackageListCommandDefinitionBase(string name) Options.Add(FormatOption); Options.Add(OutputVersionOption); Options.Add(NoRestore); + Options.Add(ProjectOption); + Options.Add(FileOption); } - public abstract string? GetFileOrDirectory(ParseResult parseResult); + public abstract Argument? GetProjectOrFileArgument(); public void EnforceOptionRules(ParseResult parseResult) { diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx index cbe3d3838aa3..57a847ae25f7 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx @@ -165,6 +165,14 @@ The '#:project' directive is invalid: {0} {0} is the inner error message. + + The '#:ref' directive is invalid: {0} + {Locked="#:ref"}{0} is the inner error message. + + + Could not find file '{0}'. + {0} is the file path. + Missing name of '{0}'. {0} is the directive name like 'package' or 'sdk'. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs index 89413dc860cc..bee6360deaaf 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs @@ -323,6 +323,7 @@ public void ReportError(TextSpan span, string message) case "property": return Property.Parse(context); case "package": return Package.Parse(context); case "project": return Project.Parse(context); + case "ref": return Ref.Parse(context); case "include" or "exclude": return IncludeOrExclude.Parse(context); default: context.ReportError(string.Format(FileBasedProgramsResources.UnrecognizedDirective, context.DirectiveKind)); @@ -587,6 +588,100 @@ void ReportError(string message) public override string ToString() => $"#:project {Name}"; } + /// + /// #:ref directive. References another file-based app as a library. + /// + public sealed class Ref : Named + { + public const string ExperimentalFileBasedProgramEnableRefDirective = nameof(ExperimentalFileBasedProgramEnableRefDirective); + + [SetsRequiredMembers] + public Ref(in ParseInfo info, string name) : base(info) + { + Name = name; + OriginalName = name; + } + + /// + /// Preserved across calls, i.e., + /// this is the original directive text as entered by the user. + /// + public string OriginalName { get; init; } + + /// + /// This is the with MSBuild $(..) vars expanded. + /// + public string? ExpandedName { get; init; } + + /// + /// The resolved full path to the referenced .cs file. + /// + public string? ResolvedPath { get; init; } + + public static new Ref? Parse(in ParseContext context) + { + var directiveText = context.DirectiveText; + if (directiveText.IsWhiteSpace()) + { + context.ReportError(string.Format(FileBasedProgramsResources.MissingDirectiveName, context.DirectiveKind)); + return null; + } + + return new Ref(context.Info, directiveText); + } + + public enum NameKind + { + /// + /// Change and . + /// + Expanded = 1, + + /// + /// Change and . + /// + Resolved = 2, + + /// + /// Change only . + /// + Final = 3, + } + + public Ref WithName(string name, NameKind kind) + { + return new Ref(Info, name) + { + OriginalName = OriginalName, + ExpandedName = kind == NameKind.Expanded ? name : ExpandedName, + ResolvedPath = kind == NameKind.Resolved ? name : ResolvedPath, + }; + } + + /// + /// Resolves the path relative to the source file's directory. + /// + public Ref EnsureResolvedPath(ErrorReporter errorReporter) + { + var sourcePath = Info.SourceFile.Path; + var sourceDirectory = Path.GetDirectoryName(sourcePath) + ?? throw new InvalidOperationException($"Source file path '{sourcePath}' does not have a containing directory."); + + var resolvedFilePath = Path.GetFullPath(Path.Combine(sourceDirectory, Name.Replace('\\', '/'))); + + if (!File.Exists(resolvedFilePath)) + { + errorReporter(Info.SourceFile.Text, sourcePath, Info.Span, + string.Format(FileBasedProgramsResources.InvalidRefDirective, + string.Format(FileBasedProgramsResources.CouldNotFindRefFile, resolvedFilePath))); + } + + return WithName(resolvedFilePath, NameKind.Resolved); + } + + public override string ToString() => $"#:ref {Name}"; + } + public enum IncludeOrExcludeKind { Include, diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt b/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt index 8beab97ae92c..bbf3d44bc01b 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt @@ -3,6 +3,7 @@ const Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.Experi const Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.ExperimentalFileBasedProgramEnableItemMapping = "ExperimentalFileBasedProgramEnableItemMapping" -> string! const Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.ExperimentalFileBasedProgramEnableTransitiveDirectives = "ExperimentalFileBasedProgramEnableTransitiveDirectives" -> string! const Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.MappingPropertyName = "FileBasedProgramsItemMapping" -> string! +const Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective = "ExperimentalFileBasedProgramEnableRefDirective" -> string! Microsoft.DotNet.FileBasedPrograms.CSharpDirective Microsoft.DotNet.FileBasedPrograms.CSharpDirective.CSharpDirective(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info) -> void Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude @@ -71,6 +72,20 @@ Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.Property(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info) -> void Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.Value.get -> string! Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.Value.init -> void +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.EnsureResolvedPath(Microsoft.DotNet.FileBasedPrograms.ErrorReporter! errorReporter) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref! +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.ExpandedName.get -> string? +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.ExpandedName.init -> void +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind.Expanded = 1 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind.Final = 3 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind.Resolved = 2 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.OriginalName.get -> string! +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.OriginalName.init -> void +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.Ref(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info, string! name) -> void +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.ResolvedPath.get -> string? +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.ResolvedPath.init -> void +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.WithName(string! name, Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind kind) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref! Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk.Sdk(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info) -> void Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk.Version.get -> string? @@ -123,6 +138,7 @@ override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.ToS override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package.ToString() -> string! override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.ToString() -> string! override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.ToString() -> string! +override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.ToString() -> string! override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk.ToString() -> string! override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Shebang.ToString() -> string! override Microsoft.DotNet.FileBasedPrograms.SourceFile.GetHashCode() -> int @@ -134,6 +150,7 @@ static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package.Parse(in Micro static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Named? static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project? static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property? +static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref? static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk? static Microsoft.DotNet.FileBasedPrograms.ErrorReporters.CreateCollectingReporter(out System.Collections.Immutable.ImmutableArray.Builder! builder) -> Microsoft.DotNet.FileBasedPrograms.ErrorReporter! static Microsoft.DotNet.FileBasedPrograms.ExternalHelpers.CombineHashCodes(int value1, int value2) -> int diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf index fccd6ec81449..a5cc8144f6ea 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf @@ -17,6 +17,11 @@ Nenašel se projekt ani adresář {0}. + + Could not find file '{0}'. + Soubor {0} nebyl nalezen. + {0} is the file path. + error chyba @@ -57,6 +62,11 @@ Direktiva #:project je neplatná: {0} {0} is the inner error message. + + The '#:ref' directive is invalid: {0} + Direktiva #:ref je neplatná: {0}. + {Locked="#:ref"}{0} is the inner error message. + Missing name of '{0}'. Chybí název pro: {0}. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf index acfc69549e69..0a2844c607d6 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf @@ -17,6 +17,11 @@ Das Projekt oder Verzeichnis "{0}" wurde nicht gefunden. + + Could not find file '{0}'. + Die Datei "{0}" konnte nicht gefunden werden. + {0} is the file path. + error Fehler @@ -57,6 +62,11 @@ Die Anweisung „#:p roject“ ist ungültig: {0} {0} is the inner error message. + + The '#:ref' directive is invalid: {0} + Die „#:ref“-Direktive ist ungültig: {0} + {Locked="#:ref"}{0} is the inner error message. + Missing name of '{0}'. Fehlender Name der Anweisung „{0}“. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf index 7fcfcf9a443e..3d3f76062d7b 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf @@ -17,6 +17,11 @@ No se encuentra el proyecto o directorio "{0}". + + Could not find file '{0}'. + No se pudo encontrar el archivo '{0}'. + {0} is the file path. + error error @@ -57,6 +62,11 @@ La directiva "#:project" no es válida: {0} {0} is the inner error message. + + The '#:ref' directive is invalid: {0} + The '#:ref' directive is invalid: {0} + {Locked="#:ref"}{0} is the inner error message. + Missing name of '{0}'. Falta el nombre de "{0}". diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf index 88c34c93ba8a..c3cb53dfa9e0 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf @@ -17,6 +17,11 @@ Projet ou répertoire '{0}' introuvable. + + Could not find file '{0}'. + Impossible de trouver le fichier '{0}'. + {0} is the file path. + error erreur @@ -57,6 +62,11 @@ La directive « #:project » n’est pas valide : {0} {0} is the inner error message. + + The '#:ref' directive is invalid: {0} + The '#:ref' directive is invalid: {0} + {Locked="#:ref"}{0} is the inner error message. + Missing name of '{0}'. Nom manquant pour « {0} ». diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf index 655d5b367356..08aa5428a90b 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf @@ -17,6 +17,11 @@ Non sono stati trovati progetti o directory `{0}`. + + Could not find file '{0}'. + Il file '{0}' non è stato trovato. + {0} is the file path. + error errore @@ -57,6 +62,11 @@ La direttiva '#:project' non è valida: {0} {0} is the inner error message. + + The '#:ref' directive is invalid: {0} + La direttiva "#:ref" non è valida: {0} + {Locked="#:ref"}{0} is the inner error message. + Missing name of '{0}'. Manca il nome di '{0}'. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf index 2d2cc195c23a..259a6b3fb80c 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf @@ -17,6 +17,11 @@ プロジェクトまたはディレクトリ `{0}` が見つかりませんでした。 + + Could not find file '{0}'. + ファイル '{0}' が見つかりませんでした。 + {0} is the file path. + error エラー @@ -57,6 +62,11 @@ '#:p roject' ディレクティブが無効です: {0} {0} is the inner error message. + + The '#:ref' directive is invalid: {0} + '#:ref' ディレクティブが無効です: {0} + {Locked="#:ref"}{0} is the inner error message. + Missing name of '{0}'. '{0}' の名前がありません。 diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf index 7082b47f9aa8..75f293801c3a 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf @@ -17,6 +17,11 @@ 프로젝트 또는 디렉터리 {0}을(를) 찾을 수 없습니다. + + Could not find file '{0}'. + '{0}' 파일을 찾을 수 없습니다. + {0} is the file path. + error 오류 @@ -57,6 +62,11 @@ '#:p roject' 지시문이 잘못되었습니다. {0} {0} is the inner error message. + + The '#:ref' directive is invalid: {0} + The '#:ref' directive is invalid: {0} + {Locked="#:ref"}{0} is the inner error message. + Missing name of '{0}'. '{0}' 이름이 없습니다. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf index 4cd99b2c63c3..70bc479286d5 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf @@ -17,6 +17,11 @@ Nie można odnaleźć projektu ani katalogu „{0}”. + + Could not find file '{0}'. + Nie można odnaleźć pliku '{0}'. + {0} is the file path. + error błąd @@ -57,6 +62,11 @@ Dyrektywa „#:project” jest nieprawidłowa: {0} {0} is the inner error message. + + The '#:ref' directive is invalid: {0} + The '#:ref' directive is invalid: {0} + {Locked="#:ref"}{0} is the inner error message. + Missing name of '{0}'. Brak nazwy „{0}”. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf index a5ef266abb7e..b389de338664 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf @@ -17,6 +17,11 @@ Não foi possível encontrar o projeto ou diretório ‘{0}’. + + Could not find file '{0}'. + Não foi possível encontrar arquivo "{0}". + {0} is the file path. + error erro @@ -57,6 +62,11 @@ A diretiva '#:project' é inválida:{0} {0} is the inner error message. + + The '#:ref' directive is invalid: {0} + A diretiva ''#:ref'' é inválida: {0} + {Locked="#:ref"}{0} is the inner error message. + Missing name of '{0}'. Nome de '{0}' ausente. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf index 3405fe15916b..bc0686e203f6 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf @@ -17,6 +17,11 @@ Не удалось найти проект или каталог "{0}". + + Could not find file '{0}'. + Не удалось найти файл "{0}". + {0} is the file path. + error ошибка @@ -57,6 +62,11 @@ Недопустимая директива "#:project": {0} {0} is the inner error message. + + The '#:ref' directive is invalid: {0} + Недопустимая директива "#:ref": {0} + {Locked="#:ref"}{0} is the inner error message. + Missing name of '{0}'. Отсутствует имя "{0}". diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf index 40ee3f703b98..3a2e8533ee02 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf @@ -17,6 +17,11 @@ `{0}` projesi veya dizini bulunamadı. + + Could not find file '{0}'. + '{0}' dosyası bulunamadı. + {0} is the file path. + error hata @@ -57,6 +62,11 @@ ‘#:project’ yönergesi geçersizdir: {0} {0} is the inner error message. + + The '#:ref' directive is invalid: {0} + The '#:ref' directive is invalid: {0} + {Locked="#:ref"}{0} is the inner error message. + Missing name of '{0}'. '{0}' adı eksik. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf index b5c92edfd759..1cd4525ecc9c 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf @@ -17,6 +17,11 @@ 找不到项目或目录“{0}”。 + + Could not find file '{0}'. + 找不到文件“{0}”。 + {0} is the file path. + error 错误 @@ -57,6 +62,11 @@ '#:project' 指令无效: {0} {0} is the inner error message. + + The '#:ref' directive is invalid: {0} + The '#:ref' directive is invalid: {0} + {Locked="#:ref"}{0} is the inner error message. + Missing name of '{0}'. 缺少 '{0}' 的名称。 diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf index d65f38fe59f2..7d2c81a0428d 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf @@ -17,6 +17,11 @@ 找不到專案或目錄 `{0}`。 + + Could not find file '{0}'. + 找不到檔案 '{0}'。 + {0} is the file path. + error 錯誤 @@ -57,6 +62,11 @@ '#:project' 指示詞無效: {0} {0} is the inner error message. + + The '#:ref' directive is invalid: {0} + The '#:ref' directive is invalid: {0} + {Locked="#:ref"}{0} is the inner error message. + Missing name of '{0}'. 缺少 '{0}' 的名稱。 diff --git a/src/Cli/dotnet/Commands/CliCommandStrings.resx b/src/Cli/dotnet/Commands/CliCommandStrings.resx index b0aa3faeb66b..08954c1028c5 100644 --- a/src/Cli/dotnet/Commands/CliCommandStrings.resx +++ b/src/Cli/dotnet/Commands/CliCommandStrings.resx @@ -1263,11 +1263,6 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man Specify only one package reference to remove. - - Removed '{0}' directives ({1}) for '{2}' from: {3} - {0} is a directive kind (like '#:package'). {1} is number of removed directives. - {2} is directive key (e.g., package name). {3} is file path from which directives were removed. - Command names conflict. Command names are case insensitive. {0} @@ -1570,6 +1565,10 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man The XML file that contains the list of packages to be stored. + + Multiple referenced files would be converted into the same directory: '{0}' + {0} is the target directory path. + Project(s) @@ -1735,6 +1734,22 @@ The default is to publish a framework-dependent application. Couldn't find a project to run. Ensure a project exists in {0}, or pass the path to the project using {1}. + + Warning: '{0}' appears to be a file-based app but was passed as an argument to the project '{1}'. To run it as a file-based app, use 'dotnet run --file {0}'. To pass it as an application argument, use 'dotnet run -- {0}' to suppress this warning. + {0} is the file path argument. {1} is the project file path.{Locked="dotnet run --file"}{Locked="dotnet run --"} + + + Warning: '{0}' appears to be a file-based app but was treated as an MSBuild argument. To treat it as a file-based app, use 'dotnet {1} {0}'. + {0} is the file path argument. {1} is the command name (e.g. build, clean, publish).{Locked="dotnet"} + + + Warning: '{0}' looks like a file-based app but the file was not found, and it was treated as an MSBuild argument. + {0} is the .cs file path argument. + + + Warning: '{0}' looks like a file-based app but the file was not found, and it was passed as an argument to the project '{1}'. To pass it as an application argument, use 'dotnet run -- {0}' to suppress this warning. + {0} is the .cs file path argument. {1} is the project file path.{Locked="dotnet run --"} + Unable to proceed with project '{0}'. Ensure you have a runnable project type. diff --git a/src/Cli/dotnet/Commands/DotNetCommandFactory.cs b/src/Cli/dotnet/Commands/DotNetCommandFactory.cs index 56d8f1dc4f59..cb096e20a01a 100644 --- a/src/Cli/dotnet/Commands/DotNetCommandFactory.cs +++ b/src/Cli/dotnet/Commands/DotNetCommandFactory.cs @@ -8,8 +8,10 @@ using Microsoft.DotNet.Cli.CommandLine; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli.Utils.Extensions; using Microsoft.DotNet.ProjectTools; using NuGet.Frameworks; +using Microsoft.DotNet.Cli.Commands; namespace Microsoft.DotNet.Cli; @@ -72,6 +74,26 @@ internal static CommandBase CreateVirtualOrPhysicalCommand( } else { + // Warn if any argument looks like a file-based program entry point but we're falling back to MSBuild. + // This can happen when extra positional arguments prevent the single-arg file-based path from being taken, + // or when a .cs file doesn't exist (so IsValidEntryPointPath returns false). + foreach (var candidate in nonBinLogArgs) + { + if (VirtualProjectBuilder.IsValidEntryPointPath(candidate)) + { + Reporter.Error.WriteLine( + string.Format(CliCommandStrings.WarningFileArgumentPassedToMSBuild, candidate, commandDefinition.Name).Yellow()); + break; + } + + if (candidate.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) + { + Reporter.Error.WriteLine( + string.Format(CliCommandStrings.WarningCsFileArgumentPassedToMSBuild, candidate).Yellow()); + break; + } + } + var msbuildArgs = MSBuildArgs.AnalyzeMSBuildArguments([.. forwardedArgs, .. args], [.. optionsToUseWhenParsingMSBuildFlags]); msbuildArgs = transformer?.Invoke(msbuildArgs) ?? msbuildArgs; return createPhysicalCommand(msbuildArgs, msbuildPath); diff --git a/src/Cli/dotnet/Commands/NuGet/NuGetCommand.cs b/src/Cli/dotnet/Commands/NuGet/NuGetCommand.cs index 9a68c31b22e7..296e57ede7db 100644 --- a/src/Cli/dotnet/Commands/NuGet/NuGetCommand.cs +++ b/src/Cli/dotnet/Commands/NuGet/NuGetCommand.cs @@ -6,19 +6,36 @@ using System.CommandLine; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.ProjectTools; namespace Microsoft.DotNet.Cli.Commands.NuGet; -public class NuGetCommand +internal class NuGetCommand { - public static int Run(string[] args) + public static int Run(string[] args, bool isFileBasedApp = false) { - return Run(args, new NuGetCommandRunner()); + return Run(args, isFileBasedApp + ? new InProcessNuGetCommandRunner(NuGetVirtualProjectBuilder.Instance) + : new NuGetCommandRunner()); } public static int Run(ParseResult parseResult) { - return Run(parseResult.GetArguments(), new NuGetCommandRunner()); + ICommandRunner runner; + + if (parseResult.CommandResult.Command.Name == "why" + && parseResult.CommandResult.Command.Arguments.FirstOrDefault() is Argument pathArg + && parseResult.GetValue(pathArg) is { } path + && VirtualProjectBuilder.IsValidEntryPointPath(path)) + { + runner = new InProcessNuGetCommandRunner(NuGetVirtualProjectBuilder.Instance); + } + else + { + runner = new NuGetCommandRunner(); + } + + return Run(parseResult.GetArguments(), runner); } public static int Run(string[] args, ICommandRunner nugetCommandRunner) @@ -43,11 +60,28 @@ private class NuGetCommandRunner : ICommandRunner public int Run(string[] args) { var nugetApp = new NuGetForwardingApp(args); - nugetApp.WithEnvironmentVariable("DOTNET_HOST_PATH", GetDotnetPath()); + nugetApp.WithEnvironmentVariable(EnvironmentVariableNames.DOTNET_HOST_PATH, GetDotnetPath()); return nugetApp.Execute(); } } + private class InProcessNuGetCommandRunner(NuGetVirtualProjectBuilder virtualProjectBuilder) : ICommandRunner + { + public int Run(string[] args) + { + var originalDotNetHostPath = Environment.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_HOST_PATH); + Environment.SetEnvironmentVariable(EnvironmentVariableNames.DOTNET_HOST_PATH, GetDotnetPath()); + try + { + return global::NuGet.CommandLine.XPlat.Program.Run(args, virtualProjectBuilder); + } + finally + { + Environment.SetEnvironmentVariable(EnvironmentVariableNames.DOTNET_HOST_PATH, originalDotNetHostPath); + } + } + } + private static string GetDotnetPath() { return new Muxer().MuxerPath; diff --git a/src/Cli/dotnet/Commands/NuGet/NuGetVirtualProjectBuilder.cs b/src/Cli/dotnet/Commands/NuGet/NuGetVirtualProjectBuilder.cs new file mode 100644 index 000000000000..a37f3e2c7aa1 --- /dev/null +++ b/src/Cli/dotnet/Commands/NuGet/NuGetVirtualProjectBuilder.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.DotNet.Cli.Commands.Package; +using Microsoft.DotNet.Cli.Commands.Run; +using Microsoft.DotNet.FileBasedPrograms; +using Microsoft.DotNet.ProjectTools; +using NuGet.CommandLine.XPlat; + +namespace Microsoft.DotNet.Cli.Commands.NuGet; + +internal sealed class NuGetVirtualProjectBuilder : IVirtualProjectBuilder +{ + public static NuGetVirtualProjectBuilder Instance => field ??= new(); + + private NuGetVirtualProjectBuilder() { } + + public bool IsValidEntryPointPath(string entryPointFilePath) => VirtualProjectBuilder.IsValidEntryPointPath(entryPointFilePath); + + public string GetVirtualProjectPath(string entryPointFilePath) => VirtualProjectBuilder.GetVirtualProjectPath(entryPointFilePath); + + public ProjectRootElement CreateProjectRootElement(string entryPointFilePath, ProjectCollection projectCollection) + { + if (!Path.IsPathFullyQualified(entryPointFilePath)) + { + throw new ArgumentException($"'{entryPointFilePath}' is not a fully qualified path.", paramName: nameof(entryPointFilePath)); + } + + var builder = new VirtualProjectBuilder(entryPointFilePath, VirtualProjectBuildingCommand.TargetFramework); + + builder.CreateProjectInstance( + projectCollection, + ErrorReporters.IgnoringReporter, + project: out _, + out var projectRootElement, + evaluatedDirectives: out _); + + return projectRootElement; + } + + public void SaveProject(string entryPointFilePath, ProjectRootElement projectRootElement) + { + VirtualProjectPackageReflector.ReflectChangesToDirectives(projectRootElement, entryPointFilePath); + } +} diff --git a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs index 7c2bbed46ac4..ceba7066a361 100644 --- a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs +++ b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs @@ -3,17 +3,12 @@ using System.CommandLine; using System.Diagnostics; -using Microsoft.Build.Construction; -using Microsoft.Build.Evaluation; -using Microsoft.CodeAnalysis; using Microsoft.DotNet.Cli.CommandLine; using Microsoft.DotNet.Cli.Commands.MSBuild; using Microsoft.DotNet.Cli.Commands.NuGet; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Utils; -using Microsoft.DotNet.FileBasedPrograms; using Microsoft.DotNet.ProjectTools; -using NuGet.ProjectModel; namespace Microsoft.DotNet.Cli.Commands.Package.Add; @@ -31,16 +26,14 @@ public override int Execute() { var (fileOrDirectory, allowedAppKinds) = PackageCommandParser.ProcessPathOptions(Definition.FileOption, Definition.ProjectOption, Definition.GetProjectOrFileArgument(), _parseResult); - if (allowedAppKinds.HasFlag(AppKinds.FileBased) && VirtualProjectBuilder.IsValidEntryPointPath(fileOrDirectory)) - { - return ExecuteForFileBasedApp(fileOrDirectory); - } + bool isFileBasedApp = allowedAppKinds.HasFlag(AppKinds.FileBased) && VirtualProjectBuilder.IsValidEntryPointPath(fileOrDirectory); - Debug.Assert(allowedAppKinds.HasFlag(AppKinds.ProjectBased)); + Debug.Assert(isFileBasedApp || allowedAppKinds.HasFlag(AppKinds.ProjectBased)); string projectFilePath; if (!File.Exists(fileOrDirectory)) { + Debug.Assert(!isFileBasedApp); projectFilePath = MsbuildProject.GetProjectFileFromDirectory(fileOrDirectory); } else @@ -48,11 +41,15 @@ public override int Execute() projectFilePath = fileOrDirectory; } + if (isFileBasedApp) + { + projectFilePath = Path.GetFullPath(projectFilePath); + } + var tempDgFilePath = string.Empty; if (!_parseResult.GetValue(Definition.NoRestoreOption)) { - try { // Create a Dependency Graph file for the project @@ -64,46 +61,69 @@ public override int Execute() throw new GracefulException(string.Format(CliCommandStrings.CmdDGFileIOException, projectFilePath), ioex); } - GetProjectDependencyGraph(projectFilePath, tempDgFilePath); + GetProjectDependencyGraph(projectFilePath, tempDgFilePath, isFileBasedApp); } - var result = NuGetCommand.Run( - TransformArgs( - _packageId, - tempDgFilePath, - projectFilePath)); + var args = TransformArgs( + _packageId, + tempDgFilePath, + projectFilePath); + + var result = NuGetCommand.Run(args, isFileBasedApp); + DisposeTemporaryFile(tempDgFilePath); return result; } - private static void GetProjectDependencyGraph(string projectFilePath, string dgFilePath) + private static void GetProjectDependencyGraph(string projectFilePath, string dgFilePath, bool isFileBasedApp) { - List args = - [ - // Pass the project file path - projectFilePath, - - // Pass the task as generate restore Dependency Graph file - "-target:GenerateRestoreGraphFile", + int result; + if (isFileBasedApp) + { + result = new VirtualProjectBuildingCommand( + projectFilePath, + MSBuildArgs + .FromProperties(new Dictionary + { + { "RestoreGraphOutputPath", dgFilePath }, + { "RestoreRecursive", "false" }, + { "RestoreDotnetCliToolReferences", "false" }, + }.AsReadOnly()) + .CloneWithVerbosity(VerbosityOptions.quiet) + .CloneWithAdditionalTargets("GenerateRestoreGraphFile")) + { + NoRestore = true, + NoCache = true, + NoWriteBuildMarkers = true, + }.Execute(); + } + else + { + result = new MSBuildForwardingApp( + [ + // Pass the project file path + projectFilePath, - // Pass Dependency Graph file output path - $"-property:RestoreGraphOutputPath=\"{dgFilePath}\"", + // Pass the task as generate restore Dependency Graph file + "-target:GenerateRestoreGraphFile", - // Turn off recursive restore - $"-property:RestoreRecursive=false", + // Pass Dependency Graph file output path + $"-property:RestoreGraphOutputPath=\"{dgFilePath}\"", - // Turn off restore for Dotnet cli tool references so that we do not generate extra dg specs - $"-property:RestoreDotnetCliToolReferences=false", + // Turn off recursive restore + "-property:RestoreRecursive=false", - // Output should not include MSBuild version header - "--nologo", + // Turn off restore for Dotnet cli tool references so that we do not generate extra dg specs + "-property:RestoreDotnetCliToolReferences=false", - // Set verbosity to quiet to avoid cluttering the output for this 'inner' build - "-v:quiet" - ]; + // Output should not include MSBuild version header + "--nologo", - var result = new MSBuildForwardingApp(args).Execute(); + // Set verbosity to quiet to avoid cluttering the output for this 'inner' build + "-v:quiet" + ]).Execute(); + } if (result != 0) { @@ -152,210 +172,4 @@ private string[] TransformArgs(PackageIdentityWithRange packageId, string tempDg return [.. args]; } - - // More logic should live in NuGet: https://github.com/NuGet/Home/issues/14390 - private int ExecuteForFileBasedApp(string path) - { - // Check disallowed options. - ReadOnlySpan