Skip to content

Commit 24d1dff

Browse files
fix(project): resolve TargetFramework from Directory.Build.props
When a .csproj/.fsproj/.vbproj (or the project matched via a solution) doesn't define a TargetFramework directly, MSBuild pulls it from a Directory.Build.props further up the tree. The project segment didn't account for that, so centrally-configured solutions showed no version. resolves #7670 Entire-Checkpoint: 55d5cdc9f52a
1 parent 84d806d commit 24d1dff

3 files changed

Lines changed: 74 additions & 8 deletions

File tree

src/segments/project.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,18 @@ func (n *Project) getDotnetProject(item ProjectItem) *ProjectData {
325325
}
326326
}
327327

328+
// mirror MSBuild's implicit import of Directory.Build.props when the
329+
// project/solution itself does not define a TargetFramework
330+
if target == "" {
331+
if props, err := n.env.HasParentFilePath("Directory.Build.props", false); err == nil {
332+
propsContent := n.env.FileContent(props.Path)
333+
values = regex.FindNamedRegexMatch(tag, propsContent)
334+
if len(values) != 0 {
335+
target = values["TFM"]
336+
}
337+
}
338+
}
339+
328340
if target == "" {
329341
log.Error(fmt.Errorf("cannot extract TFM from %s project file", name))
330342
}

src/segments/project_test.go

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package segments
22

33
import (
4+
"errors"
45
"io/fs"
56
"os"
67
"path/filepath"
78
"testing"
89

10+
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
911
"github.com/jandedobbeleer/oh-my-posh/src/runtime/mock"
1012
"github.com/jandedobbeleer/oh-my-posh/src/segments/options"
1113

@@ -642,6 +644,7 @@ func TestDotnetProject(t *testing.T) {
642644
},
643645
})
644646
env.On("FileContent", tc.FileName).Return(tc.ProjectContents)
647+
env.On("HasParentFilePath", "Directory.Build.props", false).Return((*runtime.FileInfo)(nil), errors.New("not found"))
645648
pkg := &Project{}
646649
pkg.Init(options.Map{}, env)
647650
assert.Equal(t, tc.ExpectedEnabled, pkg.Enabled(), tc.Case)
@@ -653,13 +656,14 @@ func TestDotnetProject(t *testing.T) {
653656

654657
func TestDotnetSolutionResolvesTargetFramework(t *testing.T) {
655658
cases := []struct {
656-
Case string
657-
SolutionFile string
658-
SubDirs map[string][]fs.DirEntry // path -> entries returned by LsDir
659-
FileContents map[string]string // path -> file content
660-
Options options.Map
661-
ExpectedString string
662-
ExpectedEnabled bool
659+
SubDirs map[string][]fs.DirEntry
660+
FileContents map[string]string
661+
Options options.Map
662+
DirectoryBuildProps *runtime.FileInfo
663+
Case string
664+
SolutionFile string
665+
ExpectedString string
666+
ExpectedEnabled bool
663667
}{
664668
{
665669
Case: ".sln with .csproj one level deep",
@@ -811,6 +815,49 @@ func TestDotnetSolutionResolvesTargetFramework(t *testing.T) {
811815
ExpectedEnabled: true,
812816
ExpectedString: "MyApp",
813817
},
818+
{
819+
Case: "TFM defined centrally in Directory.Build.props",
820+
SolutionFile: "MyApp.sln",
821+
SubDirs: map[string][]fs.DirEntry{
822+
"posh": {
823+
&MockDirEntry{name: "MyApp.sln"},
824+
&MockDirEntry{name: "App", isDir: true},
825+
},
826+
filepath.Join("posh", "App"): {
827+
&MockDirEntry{name: "App.csproj"},
828+
},
829+
},
830+
FileContents: map[string]string{
831+
"MyApp.sln": "",
832+
filepath.Join("App", "App.csproj"): "<Project><PropertyGroup></PropertyGroup></Project>",
833+
filepath.Join("posh", "Directory.Build.props"): "<Project><PropertyGroup><TargetFramework>net10.0</TargetFramework></PropertyGroup></Project>",
834+
},
835+
DirectoryBuildProps: &runtime.FileInfo{
836+
Path: filepath.Join("posh", "Directory.Build.props"),
837+
ParentFolder: "posh",
838+
},
839+
ExpectedEnabled: true,
840+
ExpectedString: "MyApp  net10.0",
841+
},
842+
{
843+
Case: "no TFM anywhere, Directory.Build.props not found",
844+
SolutionFile: "MyApp.sln",
845+
SubDirs: map[string][]fs.DirEntry{
846+
"posh": {
847+
&MockDirEntry{name: "MyApp.sln"},
848+
&MockDirEntry{name: "App", isDir: true},
849+
},
850+
filepath.Join("posh", "App"): {
851+
&MockDirEntry{name: "App.csproj"},
852+
},
853+
},
854+
FileContents: map[string]string{
855+
"MyApp.sln": "",
856+
filepath.Join("App", "App.csproj"): "<Project><PropertyGroup></PropertyGroup></Project>",
857+
},
858+
ExpectedEnabled: true,
859+
ExpectedString: "MyApp",
860+
},
814861
}
815862

816863
for _, tc := range cases {
@@ -835,6 +882,12 @@ func TestDotnetSolutionResolvesTargetFramework(t *testing.T) {
835882
env.On("FileContent", path).Return(content)
836883
}
837884

885+
if tc.DirectoryBuildProps != nil {
886+
env.On("HasParentFilePath", "Directory.Build.props", false).Return(tc.DirectoryBuildProps, nil)
887+
} else {
888+
env.On("HasParentFilePath", "Directory.Build.props", false).Return((*runtime.FileInfo)(nil), errors.New("not found"))
889+
}
890+
838891
opts := tc.Options
839892
if opts == nil {
840893
opts = options.Map{}

website/docs/segments/system/project.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ Supports:
1919
- PHP project (`composer.json`)
2020
- Dart project (`pubspec.yaml`)
2121
- Any nuspec based project (`*.nuspec`, first file match info is displayed)
22-
- .NET project (`*.sln`, `*.slnf`, `*.slnx`, `*.csproj`, `*.vbproj` or `*.fsproj`, first file match info is displayed)
22+
- .NET project (`*.sln`, `*.slnf`, `*.slnx`, `*.csproj`, `*.vbproj` or `*.fsproj`, first file match info is displayed). When the
23+
matched project doesn't define a `TargetFramework`, it's resolved from the nearest `Directory.Build.props` up the directory tree
2324
- Julia project (`JuliaProject.toml`, `Project.toml`)
2425
- PowerShell project (`*.psd1`, first file match info is displayed)
2526
- Lean 4 project (`lakefile.lean`, `lakefile.toml`)

0 commit comments

Comments
 (0)