cmd/cpp/functions_framework/main.go (289 lines of code) (raw):
// Copyright 2021 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.
// Implements cpp/functions_framework buildpack.
// The functions_framework buildpack converts a functionn into an application and sets up the execution environment.
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
"time"
"github.com/GoogleCloudPlatform/buildpacks/pkg/env"
gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack"
)
const (
mainLayerName = "main"
buildLayerName = "build"
vcpkgCacheLayerName = "vcpkg-binary-cache"
vcpkgLayerName = "vcpkg"
vcpkgTarballPrefix = "https://github.com/microsoft/vcpkg/archive/refs/tags"
vcpkgVersion = "2024.07.12"
vcpkgVersionPrefix = "Vcpkg package management program version "
vcpkgTripletName = "x64-linux-nodebug"
installLayerName = "cpp"
functionsFrameworkNamespace = "::google::cloud::functions"
)
type signatureInfo struct {
ReturnType string
ArgumentType string
WrapperType string
Eval string
}
var (
vcpkgURL = fmt.Sprintf("%s/%s.tar.gz", vcpkgTarballPrefix, vcpkgVersion)
mainTmpl = template.Must(template.New("mainV0").Parse(mainTextTemplateV0))
declarativeSignature = signatureInfo{
ReturnType: functionsFrameworkNamespace + "::Function",
ArgumentType: "",
WrapperType: "",
Eval: "()",
}
httpSignature = signatureInfo{
ReturnType: functionsFrameworkNamespace + "::HttpResponse",
ArgumentType: functionsFrameworkNamespace + "::HttpRequest",
WrapperType: functionsFrameworkNamespace + "::UserHttpFunction",
Eval: "",
}
cloudEventSignature = signatureInfo{
ReturnType: "void",
ArgumentType: functionsFrameworkNamespace + "::CloudEvent",
WrapperType: functionsFrameworkNamespace + "::UserCloudEventFunction",
Eval: "",
}
)
type fnInfo struct {
Target string
Namespace string
ShortName string
Signature signatureInfo
}
func main() {
gcp.Main(detectFn, buildFn)
}
func hasCppCode(ctx *gcp.Context) (bool, error) {
exists, err := ctx.FileExists("CMakeLists.txt")
if err != nil {
return false, err
}
if exists {
return true, nil
}
for _, pattern := range []string{"*.cc", "*.cxx", "*.cpp"} {
atLeastOne, err := ctx.HasAtLeastOne(pattern)
if err != nil {
return false, fmt.Errorf("finding %v files: %w", pattern, err)
}
if atLeastOne {
return true, nil
}
}
return false, nil
}
func detectFn(ctx *gcp.Context) (gcp.DetectResult, error) {
hasCpp, err := hasCppCode(ctx)
if err != nil {
return nil, err
}
if !hasCpp {
return gcp.OptOut("no C++ sources, nor a CMakeLists.txt file found"), nil
}
if _, ok := os.LookupEnv(env.FunctionTarget); ok {
return gcp.OptInEnvSet(env.FunctionTarget), nil
}
return gcp.OptOutEnvNotSet(env.FunctionTarget), nil
}
func buildFn(ctx *gcp.Context) error {
vcpkgPath, err := installVcpkg(ctx)
if err != nil {
return err
}
vcpkgCache, err := ctx.Layer(vcpkgCacheLayerName, gcp.BuildLayer, gcp.CacheLayer)
if err != nil {
return fmt.Errorf("creating %v layer: %w", vcpkgCacheLayerName, err)
}
mainLayer, err := ctx.Layer(mainLayerName)
if err != nil {
return fmt.Errorf("creating %v layer: %w", mainLayerName, err)
}
if err := ctx.SetFunctionsEnvVars(mainLayer); err != nil {
return err
}
buildLayer, err := ctx.Layer(buildLayerName, gcp.BuildLayer, gcp.CacheLayer)
if err != nil {
return fmt.Errorf("creating %v layer: %w", buildLayerName, err)
}
fn := extractFnInfo(os.Getenv(env.FunctionTarget), os.Getenv(env.FunctionSignatureType))
if err := createMainCppFile(ctx, fn, filepath.Join(mainLayer.Path, "main.cc")); err != nil {
return err
}
if err := createMainCppSupportFiles(ctx, mainLayer.Path, ctx.BuildpackRoot()); err != nil {
return err
}
installLayer, err := ctx.Layer(installLayerName, gcp.LaunchLayer)
if err != nil {
return fmt.Errorf("creating %v layer: %w", installLayerName, err)
}
vcpkgExePath := filepath.Join(vcpkgPath, "vcpkg")
cmakeExePath, err := getToolPath(ctx, vcpkgExePath, "cmake")
if err != nil {
return err
}
ninjaExePath, err := getToolPath(ctx, vcpkgExePath, "ninja")
if err != nil {
return err
}
// vcpkg is not retrying downloads at this time. Do that manually.
for i := 1; i < 32; i *= 2 {
if err := warmupVcpkg(ctx, vcpkgExePath); err == nil {
break
}
ctx.Logf("Downloading basic dependencies failed [%v], retrying in %d seconds...", err, i)
time.Sleep(time.Duration(i) * time.Second)
}
args := []string{
cmakeExePath,
"-GNinja",
"-DMAKE_BUILD_TYPE=Release",
"-DCMAKE_CXX_COMPILER=g++-8",
"-DCMAKE_C_COMPILER=gcc-8",
fmt.Sprintf("-DCMAKE_MAKE_PROGRAM=%s", ninjaExePath),
"-S", mainLayer.Path,
"-B", buildLayer.Path,
fmt.Sprintf("-DCNB_APP_DIR=%s", ctx.ApplicationRoot()),
fmt.Sprintf("-DCMAKE_INSTALL_PREFIX=%s", installLayer.Path),
fmt.Sprintf("-DVCPKG_TARGET_TRIPLET=%s", vcpkgTripletName),
fmt.Sprintf("-DCMAKE_TOOLCHAIN_FILE=%s/scripts/buildsystems/vcpkg.cmake", vcpkgPath),
}
if _, err := ctx.Exec(args, gcp.WithUserAttribution, gcp.WithEnv(
fmt.Sprintf("VCPKG_DEFAULT_BINARY_CACHE=%s", vcpkgCache.Path),
fmt.Sprintf("VCPKG_DEFAULT_HOST_TRIPLET=%s", vcpkgTripletName))); err != nil {
return err
}
if _, err := ctx.Exec([]string{cmakeExePath, "--build", buildLayer.Path, "--target", "install"}, gcp.WithUserAttribution); err != nil {
return err
}
ctx.AddWebProcess([]string{filepath.Join(installLayer.Path, "bin", "function")})
return nil
}
func warmupVcpkg(ctx *gcp.Context, vcpkgExePath string) error {
exec, err := ctx.Exec([]string{vcpkgExePath, "install", "--feature-flags=-manifests", "--only-downloads", "functions-framework-cpp"}, gcp.WithUserAttribution)
if err != nil {
return fmt.Errorf("downloading sources (exit code %d): %v", exec.ExitCode, exec.Combined)
}
return nil
}
func getToolPath(ctx *gcp.Context, vcpkgExePath string, tool string) (string, error) {
exec, err := ctx.Exec([]string{vcpkgExePath, "fetch", "--feature-flags=-manifests", tool}, gcp.WithUserAttribution)
if err != nil {
return "", fmt.Errorf("fetching %s tool path (exit code %d): %v", tool, exec.ExitCode, exec.Combined)
}
// If the tool needs to be downloaded, vcpkg now prints additional informational messages before the actual path.
// Ignore all these messages.
ss := strings.Split(exec.Stdout, "\n")
if len(ss) < 1 {
return "", fmt.Errorf("fetching %s tool path, output should have at least one newline", tool)
}
return ss[len(ss)-1], nil
}
func installVcpkg(ctx *gcp.Context) (string, error) {
vcpkg, err := ctx.Layer(vcpkgLayerName, gcp.BuildLayer, gcp.CacheLayer)
if err != nil {
return "", fmt.Errorf("creating %v layer: %w", vcpkgLayerName, err)
}
customTripletPath := filepath.Join(vcpkg.Path, "triplets", vcpkgTripletName+".cmake")
vcpkgExePath := filepath.Join(vcpkg.Path, "vcpkg")
vcpkgBaselinePath := filepath.Join(vcpkg.Path, "versions", "baseline.json")
isValid, err := validateVcpkgCache(ctx, customTripletPath, vcpkgExePath, vcpkgBaselinePath)
if err != nil {
return "", err
}
if isValid {
ctx.CacheHit(vcpkgLayerName)
return vcpkg.Path, nil
}
ctx.CacheMiss(vcpkgLayerName)
ctx.Logf("Installing vcpkg %s", vcpkgVersion)
command := fmt.Sprintf("curl --fail --show-error --silent --location --retry 3 %s | tar xz --directory %s --strip-components=1", vcpkgURL, vcpkg.Path)
if _, err := ctx.Exec([]string{"bash", "-c", command}, gcp.WithUserAttribution); err != nil {
return "", err
}
if _, err := ctx.Exec([]string{filepath.Join(vcpkg.Path, "bootstrap-vcpkg.sh")}); err != nil {
return "", err
}
if _, err := ctx.Exec([]string{"cp", filepath.Join(ctx.BuildpackRoot(), "converter", "x64-linux-nodebug.cmake"), customTripletPath}); err != nil {
return "", err
}
return vcpkg.Path, nil
}
func validateVcpkgCache(ctx *gcp.Context, customTripletPath string, vcpkgExePath string, vcpkgBaselinePath string) (bool, error) {
exists, err := ctx.FileExists(customTripletPath)
if err != nil {
return false, err
}
if !exists {
ctx.Debugf("Missing vcpkg custom triplet (%s)", customTripletPath)
return false, nil
}
exists, err = ctx.FileExists(vcpkgBaselinePath)
if err != nil {
return false, err
}
if !exists {
ctx.Debugf("Missing vcpkg baseline file (%s)", vcpkgBaselinePath)
return false, nil
}
exists, err = ctx.FileExists(vcpkgExePath)
if err != nil {
return false, err
}
if !exists {
ctx.Debugf("Missing vcpkg tool (%s)", vcpkgExePath)
return false, nil
}
return true, nil
}
func createMainCppFile(ctx *gcp.Context, fn fnInfo, main string) error {
f, err := ctx.CreateFile(main)
if err != nil {
return err
}
defer f.Close()
tmpl := mainTmpl
if err := tmpl.Execute(f, fn); err != nil {
return fmt.Errorf("executing template: %v", err)
}
return nil
}
func extractFnInfo(fnTarget string, fnSignature string) fnInfo {
info := fnInfo{
Target: fnTarget,
Namespace: "",
ShortName: fnTarget,
Signature: declarativeSignature,
}
if fnSignature == "http" {
info.Signature = httpSignature
}
if fnSignature == "cloudevent" {
info.Signature = cloudEventSignature
}
c := strings.Split(fnTarget, "::")
if len(c) != 1 {
info.ShortName = c[len(c)-1]
info.Namespace = strings.Join(c[:len(c)-1], "::")
}
return info
}
func createMainCppSupportFiles(ctx *gcp.Context, main string, buildpackRoot string) error {
if _, err := ctx.Exec([]string{"cp", filepath.Join(buildpackRoot, "converter", "CMakeLists.txt"), filepath.Join(main, "CMakeLists.txt")}); err != nil {
return err
}
vcpkgJSONDestinationFilename := filepath.Join(main, "vcpkg.json")
vcpkgJSONSourceFilename := filepath.Join(ctx.ApplicationRoot(), "vcpkg.json")
vcpkgExists, err := ctx.FileExists(vcpkgJSONSourceFilename)
if err != nil {
return err
}
if !vcpkgExists {
vcpkgJSONSourceFilename = filepath.Join(buildpackRoot, "converter", "vcpkg.json")
}
if _, err := ctx.Exec([]string{"cp", vcpkgJSONSourceFilename, vcpkgJSONDestinationFilename}); err != nil {
return err
}
return nil
}