pkg/dotnet/dotnet.go (216 lines of code) (raw):

// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package dotnet contains .NET buildpack library code. package dotnet import ( "encoding/json" "encoding/xml" "errors" "fmt" "os" "path/filepath" "strings" "github.com/GoogleCloudPlatform/buildpacks/pkg/env" gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack" ) const ( aspDotnetCore = "Microsoft.AspNetCore.App" envSdkVersion = "GOOGLE_DOTNET_SDK_VERSION" googleMin22 = "google.min.22" // EnvRuntimeVersion is the environment variable key for storing the target dotnet runtime version. EnvRuntimeVersion = "GOOGLE_ASP_NET_CORE_VERSION" // PublishLayerName is the name of the directory containing the publish layer PublishLayerName = "publish" // PublishOutputDirName is passed as the output directory for `dotnet publish`. PublishOutputDirName = "bin" ) // ProjectFiles finds all project files supported by dotnet. func ProjectFiles(ctx *gcp.Context, dir string) ([]string, error) { result, err := ctx.Exec([]string{"find", dir, "-regex", `.*\.\(cs\|fs\|vb\)proj`}, gcp.WithUserTimingAttribution) if err != nil { return nil, err } stdout := strings.TrimSpace(result.Stdout) if stdout == "" { return nil, nil } return strings.Split(stdout, "\n"), nil } // Project represents a .NET project file. type Project struct { XMLName xml.Name `xml:"Project"` PropertyGroups []PropertyGroup `xml:"PropertyGroup"` ItemGroups []ItemGroup `xml:"ItemGroup"` } // PropertyGroup contains information about a project build. type PropertyGroup struct { AssemblyName string `xml:"AssemblyName"` TargetFramework string `xml:"TargetFramework"` TargetFrameworks string `xml:"TargetFrameworks"` } // ItemGroup contains information about a project item group. type ItemGroup struct { PackageReferences []PackageReference `xml:"PackageReference"` } // PackageReference contains information about a package reference. type PackageReference struct { Include string `xml:"Include,attr"` Version string `xml:"Version,attr"` } // ReadProjectFile returns a .NET Project object. func ReadProjectFile(ctx *gcp.Context, proj string) (Project, error) { data, err := ctx.ReadFile(proj) if err != nil { return Project{}, err } return readProjectFile(data, proj) } // readProjectFile returns a .NET Project object. func readProjectFile(data []byte, proj string) (Project, error) { var p Project if err := xml.Unmarshal(data, &p); err != nil { return p, gcp.UserErrorf("unmarshalling %s: %v", proj, err) } return p, nil } // BuildableDir returns the directory of the provided GOOGLE_BUILDABLE env var. // Buildable is in the form of app, app/app.csproj, or app/app.vbproj. func BuildableDir() string { buildable := os.Getenv(env.Buildable) if strings.Contains(filepath.Ext(buildable), "proj") { return filepath.Dir(buildable) } return buildable } // RuntimeConfigJSONFiles returns all runtimeconfig.json files in 'path'. // The runtimeconfig.json file is present for compiled .NET assemblies. func RuntimeConfigJSONFiles(path string) ([]string, error) { files, err := filepath.Glob(filepath.Join(path, "*runtimeconfig.json")) if err != nil { return nil, err } if files == nil { return []string{}, nil } return files, nil } // RuntimeConfigJSON matches the structure of a runtimeconfig.json file. type RuntimeConfigJSON struct { RuntimeOptions runtimeOptions `json:"runtimeOptions"` } type framework struct { Name string `json:"name"` Version string `json:"version"` } type configProperties struct { SystemGCServer bool `json:"System.GC.Server"` } type runtimeOptions struct { TFM string `json:"tfm"` Framework framework `json:"framework"` Frameworks []framework `json:"frameworks"` ConfigProperties configProperties `json:"configProperties"` } // ReadRuntimeConfigJSON reads a given runtimeconfig.json file and returns a struct // representation of the contents. func ReadRuntimeConfigJSON(path string) (*RuntimeConfigJSON, error) { bytes, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading %q: %w", path, err) } var runCfg RuntimeConfigJSON if err := json.Unmarshal(bytes, &runCfg); err != nil { return nil, fmt.Errorf("unmarshalling %q to RuntimeConfig: %v", path, err) } return &runCfg, nil } // globalJSON represents the contents of a global.json file. type globalJSON struct { Sdk struct { Version string `json:"version"` } `json:"sdk"` } // GetSDKVersion returns the appropriate .NET SDK version to use, with the following heuristic: // 1. Return value of env variable GOOGLE_DOTNET_SDK_VERSION if present. // 2. Return value of env variable GOOGLE_RUNTIME_VERSION if present. // 3. Return SDK.Version from the .NET global.json file if present. // 4. Return an empty string by default, which will cause us to use the latest version available // on dl.google.com (see runtime.InstallTarballIfNotCached for details). func GetSDKVersion(ctx *gcp.Context) (string, error) { if version := os.Getenv(envSdkVersion); version != "" { ctx.Logf("Using .NET Core SDK version from %s: %s", envSdkVersion, version) return version, nil } if version := os.Getenv(env.RuntimeVersion); version != "" { ctx.Logf("Using .NET Core SDK version from %s: %s", env.RuntimeVersion, version) return version, nil } ctx.Logf("Looking for global.json in %v", ctx.ApplicationRoot()) gjs, err := getGlobalJSONOrNil(ctx.ApplicationRoot()) if err != nil { return "", err } if gjs != nil && gjs.Sdk.Version != "" { ctx.Logf("Using .NET Core SDK version from global.json: %s", gjs.Sdk.Version) return gjs.Sdk.Version, nil } ctx.Logf("Using latest stable .NET Core SDK version") return "", nil } func getGlobalJSONOrNil(applicationRoot string) (*globalJSON, error) { bytes, err := os.ReadFile(filepath.Join(applicationRoot, "global.json")) if err != nil { if errors.Is(err, os.ErrNotExist) { return nil, nil } return nil, fmt.Errorf("reading global.json: %w", err) } var gjs globalJSON if err := json.Unmarshal(bytes, &gjs); err != nil { return nil, gcp.UserErrorf("unmarshalling global.json: %v", err) } return &gjs, nil } // FindProjectFile finds the csproj file using the 'GOOGLE_BUILDABLE' env var and falling back with a search of the current directory. func FindProjectFile(ctx *gcp.Context) (string, error) { proj := os.Getenv(env.Buildable) if proj == "" { proj = "." } // Find the project file if proj is a directory. if fi, err := os.Stat(proj); os.IsNotExist(err) { return "", gcp.UserErrorf("%s does not exist", proj) } else if err != nil { return "", fmt.Errorf("stating %s: %v", proj, err) } else if fi.IsDir() { projFiles, err := ProjectFiles(ctx, proj) if err != nil { return "", err } if len(projFiles) != 1 { return "", gcp.UserErrorf("expected to find exactly one project file in directory %s, found %v", proj, projFiles) } proj = projFiles[0] } return proj, nil } // GetRuntimeVersion returns the value in GOOGLE_ASP_NET_CORE_VERSION, and if not set, returns // Microsoft.AspNetCore.App version in the runtimeconfig.json file found in dir. func GetRuntimeVersion(ctx *gcp.Context, dir string) (string, error) { envVarVersion := os.Getenv(EnvRuntimeVersion) if envVarVersion != "" { ctx.Logf("Determined runtime version from %v: %v", EnvRuntimeVersion, envVarVersion) return envVarVersion, nil } rtCfgVersion, rtCfgFile, rtCfgErr := getRuntimeVersionFromRtCfgDir(ctx, dir) if rtCfgErr != nil { return "", fmt.Errorf("%v was not set; when %v absent, getting version from runtimeconfig.json failed: %w", EnvRuntimeVersion, EnvRuntimeVersion, rtCfgErr) } ctx.Logf("Determined runtime version from %v: %v", rtCfgFile, rtCfgVersion) return rtCfgVersion, nil } func getRuntimeVersionFromRtCfgDir(ctx *gcp.Context, dir string) (string, string, error) { rtCfgFiles, err := RuntimeConfigJSONFiles(dir) if err != nil { return "", "", gcp.InternalErrorf("finding runtimeconfig.json: %v", err) } if len(rtCfgFiles) > 1 { return "", "", fmt.Errorf("more than one runtimeconfig.json file found: %v", rtCfgFiles) } if len(rtCfgFiles) < 1 { return "", "", fmt.Errorf("no runtimeconfig.json file was found") } ctx.Logf("Found runtimeconfig file %q", rtCfgFiles[0]) version := "" rtCfg, err := ReadRuntimeConfigJSON(rtCfgFiles[0]) if err != nil { return "", rtCfgFiles[0], fmt.Errorf("reading runtimeconfig.json: %w", err) } if rtCfg.RuntimeOptions.Framework.Name == aspDotnetCore { version = rtCfg.RuntimeOptions.Framework.Version } else { for _, fw := range rtCfg.RuntimeOptions.Frameworks { if fw.Name == aspDotnetCore { version = fw.Version break } } } if version == "" { return "", rtCfgFiles[0], fmt.Errorf("couldn't find runtime version for framework %s from "+ "runtimeconfig.json: %#v", aspDotnetCore, rtCfg) } return version, rtCfgFiles[0], nil } // RequiresGlobalizationInvariant returns true if the system lacks the OS packages necessary to // support .NET globalization. func RequiresGlobalizationInvariant(ctx *gcp.Context) bool { return ctx.StackID() == googleMin22 }