func InstallRequirements()

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
}