in pkg/python/python.go [139:277]
func InstallRequirements(ctx *gcp.Context, l *libcnb.Layer, reqs ...string) error {
// Defensive check, this should not happen in practice.
if len(reqs) == 0 {
ctx.Debugf("No requirements.txt to install, clearing layer.")
if err := ctx.ClearLayer(l); err != nil {
return fmt.Errorf("clearing layer %q: %w", l.Name, err)
}
return nil
}
currentPythonVersion, err := Version(ctx)
if err != nil {
return err
}
hash, cached, err := cache.HashAndCheck(ctx, l, dependencyHashKey,
cache.WithFiles(reqs...),
cache.WithStrings(currentPythonVersion))
if err != nil {
return err
}
// Check cache expiration to pick up new versions of dependencies that are not pinned.
expired := cacheExpired(ctx, l)
if cached && !expired {
return nil
}
if expired {
ctx.Debugf("Dependencies cache expired, clearing layer.")
}
if err := ctx.ClearLayer(l); err != nil {
return fmt.Errorf("clearing layer %q: %w", l.Name, err)
}
ctx.Logf("Installing application dependencies.")
cache.Add(ctx, l, dependencyHashKey, hash)
// Update the layer metadata.
ctx.SetMetadata(l, pythonVersionKey, currentPythonVersion)
ctx.SetMetadata(l, expiryTimestampKey, time.Now().Add(expirationTime).Format(dateFormat))
if err := ar.GeneratePythonConfig(ctx); err != nil {
return fmt.Errorf("generating Artifact Registry credentials: %w", err)
}
// History of the logic below:
//
// pip install --target has several subtle issues:
// We cannot use --upgrade: https://github.com/pypa/pip/issues/8799.
// We also cannot _not_ use --upgrade, see the requirements_bin_conflict acceptance test.
//
// Instead, we use Python per-user site-packages (https://www.python.org/dev/peps/pep-0370/)
// where we can and virtualenv where we cannot.
//
// Each requirements file is installed separately to allow the requirements.txt files
// to specify conflicting dependencies (e.g. functions-framework pins package A at 1.2.0 but
// the user's requirements.txt file pins A at 1.4.0. The user should be able to override
// the functions-framework-pinned package).
// HACK: For backwards compatibility with Python 3.7 and 3.8 on App Engine and Cloud Functions.
virtualEnv := requiresVirtualEnv()
if virtualEnv {
// --without-pip and --system-site-packages allow us to use `pip` and other packages from the
// build image and avoid reinstalling them, saving about 10MB.
// TODO(b/140775593): Use virtualenv pip after FTL is no longer used and remove from build image.
if _, err := ctx.Exec([]string{"python3", "-m", "venv", "--without-pip", "--system-site-packages", l.Path}); err != nil {
return err
}
if err := copySharedLibs(ctx, l); err != nil {
return err
}
// The VIRTUAL_ENV variable is usually set by the virtual environment's activate script.
l.SharedEnvironment.Override("VIRTUAL_ENV", l.Path)
// Use the virtual environment python3 for all subsequent commands in this buildpack, for
// subsequent buildpacks, l.Path/bin will be added by lifecycle.
if err := ctx.Setenv("PATH", filepath.Join(l.Path, "bin")+string(os.PathListSeparator)+os.Getenv("PATH")); err != nil {
return err
}
if err := ctx.Setenv("VIRTUAL_ENV", l.Path); err != nil {
return err
}
} else {
l.SharedEnvironment.Default("PYTHONUSERBASE", l.Path)
if err := ctx.Setenv("PYTHONUSERBASE", l.Path); err != nil {
return err
}
}
for _, req := range reqs {
cmd := []string{
"python3", "-m", "pip", "install",
"--requirement", req,
"--upgrade",
"--upgrade-strategy", "only-if-needed",
"--no-warn-script-location", // bin is added at run time by lifecycle.
"--no-warn-conflicts", // Needed for python37 which allowed users to override dependencies. For newer versions, we do a separate `pip check`.
"--force-reinstall", // Some dependencies may be in the build image but not run image. Later requirements.txt should override earlier.
"--no-compile", // Prevent default timestamp-based bytecode compilation. Deterministic pycs are generated in a second step below.
"--disable-pip-version-check", // If we were going to upgrade pip, we would have done it already in the runtime buildpack.
"--no-cache-dir", // We used to save this to a layer, but it made builds slower because it includes http caching of pypi requests.
}
vendorDir, isVendored := os.LookupEnv(VendorPipDepsEnv)
if isVendored {
cmd = append(cmd, "--no-index", "--find-links", vendorDir)
buildermetrics.GlobalBuilderMetrics().GetCounter(buildermetrics.PipVendorDependenciesCounterID).Increment(1)
}
if !virtualEnv {
cmd = append(cmd, "--user") // Install into user site-packages directory.
}
if _, err := ctx.Exec(cmd,
gcp.WithUserAttribution); err != nil {
return err
}
}
// Generate deterministic hash-based pycs (https://www.python.org/dev/peps/pep-0552/).
// Use the unchecked version to skip hash validation at run time (for faster startup).
result, cerr := ctx.Exec([]string{
"python3", "-m", "compileall",
"--invalidation-mode", "unchecked-hash",
"-qq", // Do not print any message (matches `pip install` behavior).
l.Path,
},
gcp.WithUserAttribution)
if cerr != nil {
if result != nil {
if result.ExitCode == 1 {
// Ignore file compilation errors (matches `pip install` behavior).
return nil
}
return fmt.Errorf("compileall: %s", result.Combined)
}
return fmt.Errorf("compileall: %v", cerr)
}
return nil
}