Skip to content

Commit

Permalink
Run autoloading separately from composer install (#25)
Browse files Browse the repository at this point in the history
* run autoloading separately from composer install

Signed-off-by: Joshua Casey <[email protected]>

* add outside vendoring app and integration test

* run autoloading from workspaceVendorDir to accomodate for custom vendor dir
  • Loading branch information
Sophie Wigmore authored Aug 2, 2022
1 parent bf959f7 commit be839d8
Show file tree
Hide file tree
Showing 14 changed files with 780 additions and 77 deletions.
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

0 comments on commit be839d8

Please sign in to comment.