Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run autoloading separately from composer install #25

Merged
merged 4 commits into from
Aug 2, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 81 additions & 32 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func Build(
logger scribe.Emitter,
composerInstallOptions DetermineComposerInstallOptions,
composerInstallExec Executable,
composerDumpAutoloadExec Executable,
composerGlobalExec Executable,
checkPlatformReqsExec Executable,
sbomGenerator SBOMGenerator,
Expand Down Expand Up @@ -79,16 +80,16 @@ func Build(
}

var composerPackagesLayer packit.Layer
var layerVendorDir string
logger.Process("Executing build process")
duration, err := clock.Measure(func() error {
composerPackagesLayer, layerVendorDir, err = runComposerInstall(
composerPackagesLayer, err = runComposerInstall(
logger,
context,
composerInstallOptions,
composerPhpIniPath,
path,
composerInstallExec,
composerDumpAutoloadExec,
workspaceVendorDir,
calculator)
return err
Expand Down Expand Up @@ -119,24 +120,6 @@ func Build(
return packit.BuildResult{}, err
}

logger.Process("Writing symlink %s => %s", workspaceVendorDir, layerVendorDir)

err = os.Symlink(layerVendorDir, workspaceVendorDir)
if err != nil { // untested
return packit.BuildResult{}, err
}

if os.Getenv(BpLogLevel) == "DEBUG" {
logger.Debug.Subprocess("Listing files in %s:", layerVendorDir)
files, err := os.ReadDir(layerVendorDir)
if err != nil { // untested
return packit.BuildResult{}, err
}
for _, f := range files {
logger.Debug.Subprocess(fmt.Sprintf("- %s", f.Name()))
}
}

err = runCheckPlatformReqs(logger, checkPlatformReqsExec, context.WorkingDir, composerPhpIniPath, path)
if err != nil {
return packit.BuildResult{}, err
Expand Down Expand Up @@ -241,23 +224,24 @@ func runComposerInstall(
composerPhpIniPath string,
path string,
composerInstallExec Executable,
composerDumpAutoloadExec Executable,
workspaceVendorDir string,
calculator Calculator) (composerPackagesLayer packit.Layer, layerVendorDir string, err error) {
calculator Calculator) (composerPackagesLayer packit.Layer, err error) {

launch, build := draft.NewPlanner().MergeLayerTypes(ComposerPackagesDependency, context.Plan.Entries)

composerPackagesLayer, err = context.Layers.Get(ComposerPackagesLayerName)
if err != nil { // untested
return packit.Layer{}, "", err
return packit.Layer{}, err
}

composerJsonPath, composerLockPath, _, _ := FindComposerFiles(context.WorkingDir)

layerVendorDir = filepath.Join(composerPackagesLayer.Path, "vendor")
layerVendorDir := filepath.Join(composerPackagesLayer.Path, "vendor")

composerLockChecksum, err := calculator.Sum(composerLockPath)
if err != nil { // untested
return packit.Layer{}, "", err
return packit.Layer{}, err
}

logger.Debug.Process("Calculated checksum of %s for composer.lock", composerLockChecksum)
Expand All @@ -273,14 +257,31 @@ func runComposerInstall(
composerPackagesLayer.Build,
composerPackagesLayer.Cache)

return composerPackagesLayer, layerVendorDir, nil
logger.Process("Writing symlink %s => %s", workspaceVendorDir, layerVendorDir)
if os.Getenv(BpLogLevel) == "DEBUG" {
logger.Debug.Subprocess("Listing files in %s:", layerVendorDir)
files, err := os.ReadDir(layerVendorDir)
if err != nil { // untested
return packit.Layer{}, err
}
for _, f := range files {
logger.Debug.Subprocess(fmt.Sprintf("- %s", f.Name()))
}
}

err = os.Symlink(layerVendorDir, workspaceVendorDir)
if err != nil { // untested
return packit.Layer{}, err
}

return composerPackagesLayer, nil
}

logger.Process("Building new layer %s", composerPackagesLayer.Path)

composerPackagesLayer, err = composerPackagesLayer.Reset()
if err != nil { // untested
return packit.Layer{}, "", err
return packit.Layer{}, err
}

composerPackagesLayer.Launch, composerPackagesLayer.Build, composerPackagesLayer.Cache = launch, build, build
Expand All @@ -295,19 +296,25 @@ func runComposerInstall(
}

if exists, err := fs.Exists(workspaceVendorDir); err != nil {
return packit.Layer{}, "", err
return packit.Layer{}, err
} else if exists {
logger.Process("Detected existing vendored packages, will run 'composer install' with those packages")
if err := fs.Copy(workspaceVendorDir, layerVendorDir); err != nil { // untested
return packit.Layer{}, "", err
return packit.Layer{}, err
}
if err := os.RemoveAll(workspaceVendorDir); err != nil { // untested
return packit.Layer{}, "", err
return packit.Layer{}, err
}
}

composerInstallBuffer := bytes.NewBuffer(nil)

// `composer install` will run with `--no-autoloader` to avoid errors from
// autoloading classes outside of the vendor directory

// Once `composer install` has run, the symlink to the working directory is
// set up, and then `composer dump-autoload` on the vendor directory from
// the working directory.
logger.Process("Running 'composer install'")

execution := pexec.Execution{
Expand All @@ -328,16 +335,58 @@ func runComposerInstall(

if err != nil {
logger.Subprocess(composerInstallBuffer.String())
return packit.Layer{}, "", err
return packit.Layer{}, err
}

logger.Debug.Subprocess(composerInstallBuffer.String())
logger.Process("Ran 'composer %s'", strings.Join(execution.Args, " "))

return composerPackagesLayer, layerVendorDir, nil
logger.Process("Writing symlink %s => %s", workspaceVendorDir, layerVendorDir)
if os.Getenv(BpLogLevel) == "DEBUG" {
logger.Debug.Subprocess("Listing files in %s:", layerVendorDir)
files, err := os.ReadDir(layerVendorDir)
if err != nil { // untested
return packit.Layer{}, err
}
for _, f := range files {
logger.Debug.Subprocess(fmt.Sprintf("- %s", f.Name()))
}
}

err = os.Symlink(layerVendorDir, workspaceVendorDir)
if err != nil { // untested
return packit.Layer{}, err
}

logger.Process("Running 'composer dump-autoload'")

composerAutoloadBuffer := bytes.NewBuffer(nil)
execution = pexec.Execution{
Args: append([]string{"dump-autoload", "--classmap-authoritative"}),
Dir: context.WorkingDir,
Env: append(os.Environ(),
"COMPOSER_NO_INTERACTION=1", // https://getcomposer.org/doc/03-cli.md#composer-no-interaction
fmt.Sprintf("COMPOSER_VENDOR_DIR=%s", workspaceVendorDir),
fmt.Sprintf("PHPRC=%s", composerPhpIniPath),
fmt.Sprintf("PATH=%s", path),
),
Stdout: composerAutoloadBuffer,
Stderr: composerAutoloadBuffer,
}

err = composerDumpAutoloadExec.Execute(execution)
if err != nil {
logger.Subprocess(composerAutoloadBuffer.String())
return packit.Layer{}, err
}

logger.Debug.Subprocess(composerAutoloadBuffer.String())
logger.Process("Ran 'composer %s'", strings.Join(execution.Args, " "))

return composerPackagesLayer, nil
}

// riteComposerPhpIni will create a PHP INI file used by Composer itself,
// writeComposerPhpIni will create a PHP INI file used by Composer itself,
// such as when running `composer global` and `composer install.
// This is created in a new ignored layer.
func writeComposerPhpIni(logger scribe.Emitter, context packit.BuildContext) (composerPhpIniPath string, err error) {
Expand Down
93 changes: 52 additions & 41 deletions build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
buffer *bytes.Buffer
installOptions *fakes.DetermineComposerInstallOptions
composerInstallExecutable *fakes.Executable
composerDumpAutoloadExecutable *fakes.Executable
composerGlobalExecutable *fakes.Executable
composerCheckPlatformReqsExecExecutable *fakes.Executable
composerInstallExecution pexec.Execution
composerDumpAutoloadExecution pexec.Execution
composerGlobalExecution pexec.Execution
composerCheckPlatformReqsExecExecution pexec.Execution
sbomGenerator *fakes.SBOMGenerator
Expand All @@ -54,6 +56,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
buffer = bytes.NewBuffer(nil)
installOptions = &fakes.DetermineComposerInstallOptions{}
composerInstallExecutable = &fakes.Executable{}
composerDumpAutoloadExecutable = &fakes.Executable{}
composerGlobalExecutable = &fakes.Executable{}
composerCheckPlatformReqsExecExecutable = &fakes.Executable{}

Expand All @@ -65,6 +68,14 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
return nil
}

composerDumpAutoloadExecutable.ExecuteCall.Stub = func(temp pexec.Execution) error {
Expect(os.MkdirAll(filepath.Join(layersDir, composer.ComposerPackagesLayerName, "vendor", "autoload.php"), os.ModeDir|os.ModePerm)).To(Succeed())
Expect(fmt.Fprint(temp.Stdout, "stdout from composer dump-autoload\n")).To(Equal(35))
Expect(fmt.Fprint(temp.Stderr, "stderr from composer dump-autoload\n")).To(Equal(35))
composerDumpAutoloadExecution = temp
return nil
}

composerGlobalExecutable.ExecuteCall.Stub = func(temp pexec.Execution) error {
Expect(os.MkdirAll(filepath.Join(layersDir, composer.ComposerGlobalLayerName, "vendor", "bin", "global-package-name"), os.ModeDir|os.ModePerm)).To(Succeed())
Expect(fmt.Fprint(temp.Stdout, "stdout from composer global\n")).To(Equal(28))
Expand Down Expand Up @@ -104,6 +115,7 @@ php 8.1.4 success
scribe.NewEmitter(buffer).WithLevel("DEBUG"),
installOptions,
composerInstallExecutable,
composerDumpAutoloadExecutable,
composerGlobalExecutable,
composerCheckPlatformReqsExecExecutable,
sbomGenerator,
Expand Down Expand Up @@ -185,6 +197,11 @@ php 8.1.4 success
Expect(composerInstallExecution.Stderr).ToNot(BeNil())
Expect(len(composerInstallExecution.Env)).To(Equal(len(os.Environ()) + 6))

Expect(composerDumpAutoloadExecution.Args).To(Equal([]string{"dump-autoload", "--classmap-authoritative"}))
Expect(composerDumpAutoloadExecution.Dir).To(Equal(workingDir))
Expect(composerDumpAutoloadExecution.Stdout).ToNot(BeNil())
Expect(composerDumpAutoloadExecution.Stderr).ToNot(BeNil())

Expect(sbomGenerator.GenerateCall.Receives.Dir).To(Equal(workingDir))
Expect(composerInstallExecution.Env).To(ContainElements(
"COMPOSER_NO_INTERACTION=1",
Expand Down Expand Up @@ -433,48 +450,19 @@ extension = bar.so
})
Expect(err).NotTo(HaveOccurred())
output := buffer.String()
Expect(output).To(Equal(fmt.Sprintf(`buildpack-name buildpack-version
Writing php.ini for composer
Writing composer-php.ini to %s
Writing php.ini contents:
'[PHP]
extension_dir = "php-extension-dir"
extension = openssl.so'
Running 'composer global require'
stdout from composer global
stderr from composer global
Ran 'composer global require --no-progress package'
Adding global Composer packages to PATH:
- global-package-name
Executing build process
Calculated checksum of default-checksum for composer.lock
Building new layer %s
Setting layer types: launch=[true], build=[false], cache=[false]
Running 'composer install'
stdout from composer install
stderr from composer install
Ran 'composer install options from fake'
Completed in 0s

Generating SBOM for %s
Completed in 0s

Writing SBOM in the following format(s):
application/vnd.cyclonedx+json
application/spdx+json

Writing symlink %s => %s
Listing files in %s:
- local-package-name
Running 'composer check-platform-reqs'
Ran 'composer check-platform-reqs', found extensions 'hello, bar'
`,
filepath.Join(layersDir, composer.ComposerPhpIniLayerName, "composer-php.ini"),
filepath.Join(layersDir, composer.ComposerPackagesLayerName),
filepath.Join(layersDir, composer.ComposerPackagesLayerName),
filepath.Join(workingDir, "vendor"),
filepath.Join(layersDir, composer.ComposerPackagesLayerName, "vendor"),
Expect(output).To(ContainSubstring("Writing php.ini for composer"))
Expect(output).To(ContainSubstring("Running 'composer global require'"))
Expect(output).To(ContainSubstring("Ran 'composer global require --no-progress package'"))
Expect(output).To(ContainSubstring(" Running 'composer install'"))
Expect(output).To(ContainSubstring("Ran 'composer install options from fake'"))
Expect(output).To(ContainSubstring(fmt.Sprintf("Writing symlink %s => %s", filepath.Join(workingDir, "vendor"),
filepath.Join(layersDir, composer.ComposerPackagesLayerName, "vendor"))))
Expect(output).To(ContainSubstring(fmt.Sprintf("Listing files in %s:", filepath.Join(layersDir, composer.ComposerPackagesLayerName, "vendor"))))
Expect(output).To(ContainSubstring("Running 'composer dump-autoload'"))
Expect(output).To(ContainSubstring("Ran 'composer dump-autoload --classmap-authoritative'"))
Expect(output).To(ContainSubstring(" Generating SBOM"))
Expect(output).To(ContainSubstring(" Running 'composer check-platform-reqs'"))
Expect(output).To(ContainSubstring(" Ran 'composer check-platform-reqs', found extensions 'hello, bar'"))
})
})

Expand Down Expand Up @@ -551,6 +539,29 @@ extension = bar.so

Expect(buffer.String()).To(ContainSubstring("error message from install"))
})

context("when composerDumpAutoloadExecution fails", func() {
it.Before(func() {
composerDumpAutoloadExecutable.ExecuteCall.Stub = func(temp pexec.Execution) error {
composerDumpAutoloadExecution = temp
_, _ = fmt.Fprint(composerDumpAutoloadExecution.Stderr, "error message from dump-autoload")
return errors.New("some error from dump-autoload")
}
})

it("logs the output", func() {
result, err := build(packit.BuildContext{
BuildpackInfo: buildpackInfo,
WorkingDir: workingDir,
Layers: packit.Layers{Path: layersDir},
Plan: buildpackPlan,
})
Expect(err).To(Equal(errors.New("some error from install")))
Expect(result).To(Equal(packit.BuildResult{}))

Expect(buffer.String()).To(ContainSubstring("error message from install"))
})
})
})

context("when generating the SBOM returns an error", func() {
Expand Down
5 changes: 4 additions & 1 deletion determine_composer_install_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,23 @@ func (_ InstallOptions) Determine() []string {
return []string{
"--no-progress",
"--no-dev",
"--no-autoloader",
}
} else if installOptionsFromEnv == "" {
return []string{
"--no-progress",
"--no-autoloader",
}
} else {
parsedOptionsFromEnv, err := shellwords.Parse(installOptionsFromEnv)
if err != nil {
return []string{
"--no-progress",
"--no-autoloader",
installOptionsFromEnv,
}
}

return append([]string{"--no-progress"}, parsedOptionsFromEnv...)
return append([]string{"--no-progress", "--no-autoloader"}, parsedOptionsFromEnv...)
}
}
Loading