cmd/python/functions_framework/main.go (97 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.
// Implements python/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"
"regexp"
"github.com/GoogleCloudPlatform/buildpacks/pkg/cloudfunctions"
"github.com/GoogleCloudPlatform/buildpacks/pkg/env"
gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack"
"github.com/GoogleCloudPlatform/buildpacks/pkg/python"
)
const (
layerName = "functions-framework"
)
var (
ffRegexp = regexp.MustCompile(`(?m)^functions-framework\b([^-]|$)`)
eggRegexp = regexp.MustCompile(`(?m)#egg=functions-framework$`)
)
func main() {
gcp.Main(detectFn, buildFn)
}
func detectFn(ctx *gcp.Context) (gcp.DetectResult, error) {
if _, ok := os.LookupEnv(env.FunctionTarget); ok {
return gcp.OptInEnvSet(env.FunctionTarget, gcp.WithBuildPlans(python.RequirementsProvidesPlan)), nil
}
return gcp.OptOutEnvNotSet(env.FunctionTarget), nil
}
func buildFn(ctx *gcp.Context) error {
if err := validateSource(ctx); err != nil {
return err
}
// Check for syntax errors to prevent failures that would only manifest at run time.
if _, err := ctx.Exec([]string{"python3", "-m", "compileall", "-f", "-q", "."}, gcp.WithStdoutTail, gcp.WithUserAttribution); err != nil {
return err
}
// Determine if the function has dependency on functions-framework.
hasFrameworkDependency := false
requirementsExists, err := ctx.FileExists("requirements.txt")
if err != nil {
return err
}
if requirementsExists {
content, err := ctx.ReadFile("requirements.txt")
if err != nil {
return err
}
hasFrameworkDependency = containsFF(string(content))
}
// Install functions-framework if necessary.
l, err := ctx.Layer(layerName, gcp.LaunchLayer, gcp.BuildLayer)
if err != nil {
return fmt.Errorf("creating %v layer: %w", layerName, err)
}
if hasFrameworkDependency {
ctx.Logf("Handling functions with dependency on functions-framework.")
if err := ctx.ClearLayer(l); err != nil {
return fmt.Errorf("clearing layer %q: %w", l.Name, err)
}
} else {
if _, isVendored := os.LookupEnv(python.VendorPipDepsEnv); isVendored {
return gcp.UserErrorf("Vendored dependencies detected, please add functions-framework to requirements.txt and download it using pip")
}
ctx.Logf("Handling functions without dependency on functions-framework.")
if err := cloudfunctions.AssertFrameworkInjectionAllowed(); err != nil {
return err
}
// The pip install is performed by the pip buildpack; see python.InstallRequirements.
ctx.Logf("Adding functions-framework requirements.txt to the list of requirements files to install.")
r := filepath.Join(ctx.BuildpackRoot(), "converter", "requirements.txt")
l.BuildEnvironment.Append(python.RequirementsFilesEnv, string(os.PathListSeparator), r)
}
if err := ctx.SetFunctionsEnvVars(l); err != nil {
return err
}
ctx.AddWebProcess([]string{"functions-framework"})
return nil
}
func validateSource(ctx *gcp.Context) error {
// Fail if the default|custom source file doesn't exist, otherwise the app will fail at runtime but still build here.
fnSource, ok := os.LookupEnv(env.FunctionSource)
if !ok {
mainPYExists, err := ctx.FileExists("main.py")
if err != nil {
return err
}
if !mainPYExists {
return gcp.UserErrorf("missing main.py and %s not specified. Either create the function in main.py or specify %s to point to the file that contains the function", env.FunctionSource, env.FunctionSource)
}
} else {
fnSourceExists, err := ctx.FileExists(fnSource)
if err != nil {
return err
}
if !fnSourceExists {
return gcp.UserErrorf("%s specified file %q but it does not exist", env.FunctionSource, fnSource)
}
}
return nil
}
func containsFF(s string) bool {
return ffRegexp.MatchString(s) || eggRegexp.MatchString(s)
}