diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..3bbbeb02 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,22 @@ +# Contributing + +## Contributing Guidelines + +All pull request authors must have a Contributor License Agreement (CLA) on-file with us. Please sign the appropriate CLA ([individual](http://cloudfoundry.org/pdfs/CFF_Individual_CLA.pdf) or [corporate](http://cloudfoundry.org/pdfs/CFF_Corporate_CLA.pdf)). + +When sending signed CLA please provide your github username in case of individual CLA or the list of github usernames that can make pull requests on behalf of your organization. + +If you are confident that you're covered under a Corporate CLA, please make sure you've publicized your membership in the appropriate Github Org, per these instructions. + +## Run the tests + +TBD + +## Pull Requests + +1. Fork the project +1. Submit a pull request + +Please include integration tests (in `integration`) and corresponding fixtures (in `fixtures`) where necessary to cover any functionality that is introduced. + +**NOTE:** When submitting a pull request, *please make sure to target the `develop` branch*, so that your changes are up-to-date and easy to integrate with the most recent work on the buildpack. Thanks! diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE new file mode 100644 index 00000000..05b87126 --- /dev/null +++ b/ISSUE_TEMPLATE @@ -0,0 +1,22 @@ +What version of Cloud Foundry and CF CLI are you using? (i.e. What is the output of running `cf curl /v2/info && cf version`? + + +What version of the buildpack you are using? + + +If you were attempting to accomplish a task, what was it you were attempting to do? + + +What did you expect to happen? + + +What was the actual behavior? + + +Can you provide a sample app? + + +Please confirm where necessary: +* [ ] I have included a log output +* [ ] My log includes an error message +* [ ] I have included steps for reproduction diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..7a0705f9 --- /dev/null +++ b/NOTICE @@ -0,0 +1,8 @@ +Copyright (c) 2018-Present CloudFoundry.org Foundation, Inc. All Rights Reserved. + +This project is licensed to you under the Apache License, Version 2.0 (the "License"). +You may not use this project except in compliance with the License. + +This project may include a number of subcomponents with separate copyright notices +and license terms. Your use of these subcomponents is subject to the terms and +conditions of the subcomponent's license, as noted in the LICENSE file. diff --git a/PULL_REQUEST_TEMPLATE b/PULL_REQUEST_TEMPLATE new file mode 100644 index 00000000..01dcb691 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE @@ -0,0 +1,11 @@ +Thanks for contributing to the buildpack. To speed up the process of reviewing your pull request please provide us with: + +* A short explanation of the proposed change: + +* An explanation of the use cases your change solves + +* [ ] I have viewed signed and have submitted the Contributor License Agreement + +* [ ] I have made this pull request to the `develop` branch + +* [ ] I have added an integration test diff --git a/README.md b/README.md new file mode 100644 index 00000000..9fa93636 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# Bundler Cloud Native Buildpack +To package this buildpack for consumption: +``` +$ ./scripts/package.sh +``` +This builds the buildpack's Go source using GOOS=linux by default. You can supply another value as the first argument to package.sh. diff --git a/buildpack.toml b/buildpack.toml new file mode 100644 index 00000000..60e104ab --- /dev/null +++ b/buildpack.toml @@ -0,0 +1,10 @@ +[buildpack] +id = "org.cloudfoundry.buildpacks.bundler" +name = "Bundler Buildpack" +version = "0.0.1" + +[[stacks]] +id = "org.cloudfoundry.stacks.cflinuxfs3" + +[[stacks]] +id = "io.buildpacks.stacks.bionic" diff --git a/bundler/bundler.go b/bundler/bundler.go new file mode 100644 index 00000000..6bd05528 --- /dev/null +++ b/bundler/bundler.go @@ -0,0 +1,67 @@ +package bundler + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" +) + +const Dependency = "bundler" + +type Bundler struct{} + +func (b Bundler) Install(location string) error { + return b.run(location, "install") +} + +func (b Bundler) run(dir string, args ...string) error { + cmd := exec.Command("bundle", args...) + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + + +func GetBundlerVersion(gemFile string) (version string, err error) { + return Version(gemFile) +} + +// TODO: depends on the following guarenteee +// the currently used bundler is a valid dependency in our buildpack.toml +func Version(gemfile string) (string, error) { + dir := filepath.Dir(gemfile) + code := ` +stdout, $stdout = $stdout, $stderr +begin + def data() + return Bundler::VERSION + end + out = data() + stdout.puts({error:nil, data:out}.to_json) +rescue => e + stdout.puts({error:e.to_s, data:nil}.to_json) +end +` + + cmd := exec.Command("ruby", "-rjson", "-rbundler", "-e", code) + cmd.Dir = dir + body, err := cmd.Output() + if err != nil { + fmt.Println(body) + return "", err + } + output := struct { + Error string `json:"error"` + Data string `json:"data"` + }{} + if err := json.Unmarshal(body, &output); err != nil { + return "", err + } + if output.Error != "" { + return "", fmt.Errorf("Running ruby: %s", output.Error) + } + return output.Data, nil +} diff --git a/cmd/build/main.go b/cmd/build/main.go new file mode 100644 index 00000000..9d06e062 --- /dev/null +++ b/cmd/build/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "bundler-cnb/bundler" + "bundler-cnb/gems" + "fmt" + "github.com/buildpack/libbuildpack/buildplan" + "github.com/cloudfoundry/libcfbuildpack/build" +) + +func main() { + fmt.Println("Implement build") +} + +func runBuild(context build.Build) (int, error) { + context.Logger.FirstLine(context.Logger.PrettyIdentity(context.Buildpack)) + + contributor, willContribute, err := gems.NewContributor(context, bundler.Bundler{}) + if err != nil { + return context.Failure(102), err + } + + if willContribute { + if err := contributor.Contribute(); err != nil { + return context.Failure(103), err + } + } + + return context.Success(buildplan.BuildPlan{}) + + + return 0, fmt.Errorf("not implemented") +} diff --git a/cmd/build/main_test.go b/cmd/build/main_test.go new file mode 100644 index 00000000..d6694596 --- /dev/null +++ b/cmd/build/main_test.go @@ -0,0 +1,26 @@ +package main + +import ( + "testing" + + . "github.com/onsi/gomega" + + "github.com/cloudfoundry/libcfbuildpack/build" + "github.com/cloudfoundry/libcfbuildpack/test" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" +) + +func TestUnitBuild(t *testing.T) { + RegisterTestingT(t) + spec.Run(t, "Build", testBuild, spec.Report(report.Terminal{})) +} + +func testBuild(t *testing.T, _ spec.G, it spec.S) { + it("always passes", func() { + f := test.NewBuildFactory(t) + code, err := runBuild(f.Build) + Expect(err).ToNot(HaveOccurred()) + Expect(code).To(Equal(build.SuccessStatusCode)) + }) +} diff --git a/cmd/detect/main.go b/cmd/detect/main.go new file mode 100644 index 00000000..d47a0f65 --- /dev/null +++ b/cmd/detect/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "bundler-cnb/bundler" + "bundler-cnb/gems" + "bundler-cnb/ruby" + "fmt" + "github.com/buildpack/libbuildpack/buildplan" + "github.com/cloudfoundry/libcfbuildpack/detect" + "github.com/cloudfoundry/libcfbuildpack/helper" + "os" + "path/filepath" +) + +func main() { + fmt.Println("Implement detect") + context, err := detect.DefaultDetect() + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to create a default detection context: %s", err) + os.Exit(101) + } + + code, err := runDetect(context) + if err != nil { + context.Logger.Info(err.Error()) + } + + os.Exit(code) +} + +// TODO: implement the following +// - install nodjs, npm, and yarn if needed +func runDetect(context detect.Detect) (int, error) { + gemfile := filepath.Join(context.Application.Root, "Gemfile") + if exists, err := helper.FileExists(gemfile); err != nil { + return context.Fail(), fmt.Errorf("error checking filepath %s", gemfile) + } else if !exists { + return context.Fail(), fmt.Errorf("unable to find Gemfile in app root") + } + + var rubyVersion, bundlerVersion string + var err error + // update how these are calculated + if rubyVersion, err = ruby.GetRubyVersion(gemfile); err != nil { + return context.Fail(), fmt.Errorf("unable to resolve ruby version %s", err) + } + + if bundlerVersion, err = bundler.GetBundlerVersion(gemfile); err != nil { + return context.Fail(), fmt.Errorf("unable to resolve bundler version %s", err) + } + + + return context.Pass(buildplan.BuildPlan{ + ruby.Dependency: buildplan.Dependency{ + Version: rubyVersion, + Metadata: buildplan.Metadata{"build": true, "launch": true}, + }, + bundler.Dependency: buildplan.Dependency{ + Version: bundlerVersion, + Metadata: buildplan.Metadata{"build": true, "launch": true}, + }, + gems.Dependency: buildplan.Dependency{ + Metadata: buildplan.Metadata{"launch": true}, + }, + }) +} diff --git a/cmd/detect/main_test.go b/cmd/detect/main_test.go new file mode 100644 index 00000000..7ff15ad0 --- /dev/null +++ b/cmd/detect/main_test.go @@ -0,0 +1,139 @@ +package main + +import ( + "bundler-cnb/bundler" + "bundler-cnb/gems" + "bundler-cnb/ruby" + "fmt" + "github.com/buildpack/libbuildpack/buildplan" + "path/filepath" + "testing" + + . "github.com/onsi/gomega" + + "github.com/cloudfoundry/libcfbuildpack/detect" + "github.com/cloudfoundry/libcfbuildpack/test" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" +) + +func TestDetect(t *testing.T) { + RegisterTestingT(t) + spec.Run(t, "Detect", testDetect, spec.Report(report.Terminal{})) +} + +// TODO: test case when Gemfile is missing +// TODO: handle case for custom named Gemfile, ex: firstGemFile +func testDetect(t *testing.T, when spec.G, it spec.S) { + var factory *test.DetectFactory + + it.Before(func() { + factory = test.NewDetectFactory(t) + }) + + when("No Gemfile present", func() { + it("detection fails", func() { + code, err := runDetect(factory.Detect) + Expect(err).To(HaveOccurred()) + Expect(code).To(Equal(detect.FailStatusCode)) + }) + }) + + when("Gemfile is present", func() { + when("Gemfile.lock was bundled with version 1.X.X", func(){ + it.Before(func(){ + GemfileString := fmt.Sprintf(`ruby '~> 3.2', '< 3.2.5' + +gem 'uglifier', '>= 1.3.0'`) + test.WriteFile(t, filepath.Join(factory.Detect.Application.Root, "Gemfile"), GemfileString) + + GemfileLockString := fmt.Sprintf(`GEM + specs: + execjs (2.7.0) + uglifier (3.1.7) + execjs (>= 0.3.0, < 3) + +PLATFORMS + ruby + +DEPENDENCIES + uglifier (>= 1.3.0) + +RUBY VERSION + ruby 2.4.2p0 + +BUNDLED WITH + 1.16.4 +`) + test.WriteFile(t, filepath.Join(factory.Detect.Application.Root, "Gemfile.lock"), GemfileLockString) + }) + + it("detection succeeds with bundler and ruby versions from the Gemfile.lock", func() { + code, err := runDetect(factory.Detect) + Expect(err).NotTo(HaveOccurred()) + Expect(code).To(Equal(detect.PassStatusCode)) + Expect(factory.Output).To(Equal(buildplan.BuildPlan{ + ruby.Dependency: buildplan.Dependency{ + Version: "'~> 3.2','< 3.2.5'", + Metadata: buildplan.Metadata{"build": true, "launch": true}, + }, + bundler.Dependency: buildplan.Dependency{ + Version: "1.16.4", + Metadata: buildplan.Metadata{"build": true, "launch": true}, + }, + gems.Dependency: buildplan.Dependency{ + Metadata: buildplan.Metadata{"launch": true}, + }, + })) + }) + }) + when("Gemfile.lock was bundled with version 1.X.X", func(){ + it.Before(func(){ + GemfileString := fmt.Sprintf(`ruby '~> 3.2' + +gem 'uglifier', '>= 1.3.0'`) + test.WriteFile(t, filepath.Join(factory.Detect.Application.Root, "Gemfile"), GemfileString) + + GemfileLockString := fmt.Sprintf(`GEM + specs: + execjs (2.7.0) + uglifier (3.1.7) + execjs (>= 0.3.0, < 3) + +PLATFORMS + ruby + +DEPENDENCIES + uglifier (>= 1.3.0) + +RUBY VERSION + ruby 2.4.2p0 + +BUNDLED WITH + 2.0.1 +`) + test.WriteFile(t, filepath.Join(factory.Detect.Application.Root, "Gemfile.lock"), GemfileLockString) + }) + it("detection succeeds with bundler and ruby versions from the Gemfile.lock", func() { + code, err := runDetect(factory.Detect) + Expect(err).NotTo(HaveOccurred()) + Expect(code).To(Equal(detect.PassStatusCode)) + Expect(factory.Output).To(Equal(buildplan.BuildPlan{ + ruby.Dependency: buildplan.Dependency{ + Version: "'~> 3.2'", + Metadata: buildplan.Metadata{"build": true, "launch": true}, + }, + bundler.Dependency: buildplan.Dependency{ + Version: "2.0.1", + Metadata: buildplan.Metadata{"build": true, "launch": true}, + }, + gems.Dependency: buildplan.Dependency{ + Metadata: buildplan.Metadata{"launch": true}, + }, + })) + }) + }) + + }) + +} diff --git a/fixtures/canary/Gemfile b/fixtures/canary/Gemfile new file mode 100644 index 00000000..ae9f7e46 --- /dev/null +++ b/fixtures/canary/Gemfile @@ -0,0 +1,3 @@ +ruby '~>2.3', '< 2.3.5' + +gem 'uglifier', '>= 1.3.0' diff --git a/fixtures/canary/Gemfile.lock b/fixtures/canary/Gemfile.lock new file mode 100644 index 00000000..af945604 --- /dev/null +++ b/fixtures/canary/Gemfile.lock @@ -0,0 +1,17 @@ +GEM + specs: + execjs (2.7.0) + uglifier (3.1.7) + execjs (>= 0.3.0, < 3) + +PLATFORMS + ruby + +DEPENDENCIES + uglifier (>= 1.3.0) + +RUBY VERSION + ruby 2.4.2p0 + +BUNDLED WITH + 1.13.2 diff --git a/fixtures/canary/test.rb b/fixtures/canary/test.rb new file mode 100644 index 00000000..e87b14b1 --- /dev/null +++ b/fixtures/canary/test.rb @@ -0,0 +1,13 @@ +stdout, $stdout = $stdout, $stderr +begin + def data() + puts Bundler::Dsl.evaluate("Gemfile", 'Gemfile.lock', {})::VERSION + puts "______________________________" + puts Bundler::VERSION + return Bundler::VERSION + end + out = data() + stdout.puts({error:nil, data:out}.to_json) +rescue => e + stdout.puts({error:e.to_s, data:nil}.to_json) +end diff --git a/gems/gems.go b/gems/gems.go new file mode 100644 index 00000000..aa1e9dfc --- /dev/null +++ b/gems/gems.go @@ -0,0 +1,102 @@ +package gems + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "github.com/buildpack/libbuildpack/application" + "github.com/cloudfoundry/libcfbuildpack/build" + "github.com/cloudfoundry/libcfbuildpack/helper" + "github.com/cloudfoundry/libcfbuildpack/layers" + "io/ioutil" + "path/filepath" +) + +const Dependency = "gems" + +type PackageManager interface { + Install(location string) error +} + +type Metadata struct { + Hash string +} + +func (m Metadata) Identity() (name string, version string) { + return Dependency, m.Hash +} + +type Contributor struct { + Metadata Metadata + buildContribution bool + launchContribution bool + pkgManager PackageManager + app application.Application + layer layers.Layer + launch layers.Layers +} + +func NewContributor(context build.Build, pkgManager PackageManager) (Contributor, bool, error) { + plan, shouldUseBundler := context.BuildPlan[Dependency] + if !shouldUseBundler { + return Contributor{}, false, nil + } + + gemFile := filepath.Join(context.Application.Root, "Gemfile") + if exists, err := helper.FileExists(gemFile); err != nil { + return Contributor{}, false, err + } else if !exists { + return Contributor{}, false, fmt.Errorf(`unable to find "Gemfile"`) + } + + buf, err := ioutil.ReadFile(gemFile) + if err != nil { + return Contributor{}, false, err + } + + hash := sha256.Sum256(buf) + + contributor := Contributor{ + app: context.Application, + pkgManager: pkgManager, + layer: context.Layers.Layer(Dependency), + launch: context.Layers, + Metadata: Metadata{hex.EncodeToString(hash[:])}, + } + + if _, ok := plan.Metadata["build"]; ok { + contributor.buildContribution = true + } + + if _, ok := plan.Metadata["launch"]; ok { + contributor.launchContribution = true + } + + return contributor, true, nil +} + +// TODO: check if the vendored dir needs to be named vendor +func (c Contributor) Contribute() error { + return c.layer.Contribute(c.Metadata, func(layer layers.Layer) error { + vendorDir := filepath.Join(c.app.Root, "vendor") + + vendored, err := helper.FileExists(vendorDir) + if err != nil { + return fmt.Errorf("unable to stat gemfile: %s", err.Error()) + } + + if vendored { + c.layer.Logger.Info("using vendored gems") + // vendored case + } else { + // not vendored + c.layer.Logger.Info("Installing gems") + } + return nil + }) + // set up the env + + // change installation behavior based on caching + + // write start command +} diff --git a/gems/gems_test.go b/gems/gems_test.go new file mode 100644 index 00000000..afb9d888 --- /dev/null +++ b/gems/gems_test.go @@ -0,0 +1,211 @@ +package gems_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/cloudfoundry/libcfbuildpack/layers" + + "github.com/buildpack/libbuildpack/buildplan" + "github.com/cloudfoundry/libcfbuildpack/test" + "bundler-cnb/gems" + "github.com/golang/mock/gomock" + . "github.com/onsi/gomega" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" +) + +//go:generate mockgen -source=gems.go -destination=mocks_test.go -package=gems_test + +func TestUnitGems(t *testing.T) { + RegisterTestingT(t) + spec.Run(t, "Gems", testGems, spec.Report(report.Terminal{})) +} + +func testGems(t *testing.T, when spec.G, it spec.S) { + when("modules.NewContributor", func() { + var ( + mockCtrl *gomock.Controller + mockPkgManager *MockPackageManager + factory *test.BuildFactory + ) + + it.Before(func() { + mockCtrl = gomock.NewController(t) + mockPkgManager = NewMockPackageManager(mockCtrl) + + factory = test.NewBuildFactory(t) + }) + + it.After(func() { + mockCtrl.Finish() + }) + + when("there is no Gemfile.lock", func() { + it("fails", func() { + factory.AddBuildPlan(gems.Dependency, buildplan.Dependency{}) + + _, _, err := gems.NewContributor(factory.Build, mockPkgManager) + Expect(err).To(HaveOccurred()) + }) + }) + + when("there is a Gemfile", func() { + it.Before(func() { + test.WriteFile( + t, + filepath.Join(factory.Build.Application.Root, "Gemfile"), + "gemfile contents", + ) + }) + + it("returns true if a build plan exists", func() { + factory.AddBuildPlan(gems.Dependency, buildplan.Dependency{}) + + _, willContribute, err := gems.NewContributor(factory.Build, mockPkgManager) + Expect(err).NotTo(HaveOccurred()) + Expect(willContribute).To(BeTrue()) + }) + + it("returns false if a build plan does not exist", func() { + _, willContribute, err := gems.NewContributor(factory.Build, mockPkgManager) + Expect(err).NotTo(HaveOccurred()) + Expect(willContribute).To(BeFalse()) + }) + + it("uses Gemfile for identity", func() { + factory.AddBuildPlan(gems.Dependency, buildplan.Dependency{}) + + contributor, _, _ := gems.NewContributor(factory.Build, mockPkgManager) + name, version := contributor.Metadata.Identity() + Expect(name).To(Equal(gems.Dependency)) + Expect(version).To(Equal("f1f1324fc1e757d0f2901b6fe7daf8f5cfdc45eaf025cf30b538589951ca78a9")) + }) + + // Gems are operating system independent + // this should just move the vendored dir to the correct location and set up the env accordingly + //when("the app is vendored", func() { + // it.Before(func() { + // test.WriteFile( + // t, + // filepath.Join(factory.Build.Application.Root, "vendored", "test_module"), + // "some module", + // ) + // }) + // + // it("contributes gems to the cache layer when included in the build plan", func() { + // factory.AddBuildPlan(gems.Dependency, buildplan.Dependency{ + // Metadata: buildplan.Metadata{"build": true}, + // }) + // + // contributor, _, err := gems.NewContributor(factory.Build, mockPkgManager) + // Expect(err).NotTo(HaveOccurred()) + // + // Expect(contributor.Contribute()).To(Succeed()) + // + // layer := factory.Build.Layers.Layer(gems.Dependency) + // Expect(layer).To(test.HaveLayerMetadata(true, true, false)) + // Expect(filepath.Join(layer.Root, "test_module")).To(BeARegularFile()) + // Expect(layer).To(test.HaveOverrideSharedEnvironment("NODE_PATH", layer.Root)) + // + // Expect(filepath.Join(factory.Build.Application.Root, "node_gems")).NotTo(BeADirectory()) + // }) + // + // it("contributes gems to the launch layer when included in the build plan", func() { + // factory.AddBuildPlan(gems.Dependency, buildplan.Dependency{ + // Metadata: buildplan.Metadata{"launch": true}, + // }) + // + // contributor, _, err := gems.NewContributor(factory.Build, mockPkgManager) + // Expect(err).NotTo(HaveOccurred()) + // + // Expect(contributor.Contribute()).To(Succeed()) + // + // Expect(factory.Build.Layers).To(test.HaveLaunchMetadata(layers.Metadata{Processes: []layers.Process{{"web", "npm start"}}})) + // + // layer := factory.Build.Layers.Layer(gems.Dependency) + // Expect(layer).To(test.HaveLayerMetadata(false, true, true)) + // Expect(filepath.Join(layer.Root, "test_module")).To(BeARegularFile()) + // Expect(layer).To(test.HaveOverrideSharedEnvironment("NODE_PATH", layer.Root)) + // + // Expect(filepath.Join(factory.Build.Application.Root, "node_gems")).NotTo(BeADirectory()) + // }) + //}) + + when.Focus("the app is not vendored", func() { + + it("contributes gems to the cache layer when included in the build plan", func() { + factory.AddBuildPlan(gems.Dependency, buildplan.Dependency{ + Metadata: buildplan.Metadata{"build": true}, + }) + + contributor, _, err := gems.NewContributor(factory.Build, mockPkgManager) + Expect(err).NotTo(HaveOccurred()) + + Expect(contributor.Contribute()).To(Succeed()) + + layer := factory.Build.Layers.Layer(gems.Dependency) + + defaultGemPath = filepath.Join(layer.Root,) + mockPkgManager.EXPECT().Install(layer).Do(func(location string) { + test.WriteFile( + t, + filepath.Join(layer, "Gemfile"), // this should be where gems are written eg GEM_PATH? + "some module", + ) + }) + + Expect(layer).To(test.HaveLayerMetadata(true, true, false)) + Expect(filepath.Join(layer.Root, "test_module")).To(BeARegularFile()) // TODO: change this to be correct + // Override env variables + environmentDefaults := map[string]string{ + "RAILS_ENV": "production", + "RACK_ENV": "production", + "RAILS_GROUPS": "assets", + "BUNDLE_WITHOUT": "development:test", + "BUNDLE_GEMFILE": "Gemfile", + "BUNDLE_BIN": filepath.Join(layer.Root, "binstubs"), + "BUNDLE_CONFIG": filepath.Join(layer.Root, "bundle_config"), + "GEM_HOME": filepath.Join(layer.Root, "gem_home"), + "GEM_PATH": strings.Join([]string{ + filepath.Join(layer.Root, "gem_home"), + filepath.Join(layer.Root, "bundler"), + }, ":"), + } + + for key, value := range environmentDefaults { + Expect(layer).To(test.HaveOverrideSharedEnvironment(key, value)) + } + + + Expect(layer).To(test.HaveOverrideSharedEnvironment("NODE_PATH", layer.Root)) + + Expect(filepath.Join(factory.Build.Application.Root, "node_gems")).NotTo(BeADirectory()) + }) + + it("contributes gems to the launch layer when included in the build plan", func() { + factory.AddBuildPlan(gems.Dependency, buildplan.Dependency{ + Metadata: buildplan.Metadata{"launch": true}, + }) + + contributor, _, err := gems.NewContributor(factory.Build, mockPkgManager) + Expect(err).NotTo(HaveOccurred()) + + Expect(contributor.Contribute()).To(Succeed()) + + // TODO: add start command + Expect(factory.Build.Layers).To(test.HaveLaunchMetadata(layers.Metadata{Processes: []layers.Process{{"", ""}}})) + + layer := factory.Build.Layers.Layer(gems.Dependency) + Expect(layer).To(test.HaveLayerMetadata(false, true, true)) + Expect(filepath.Join(layer.Root, "test_module")).To(BeARegularFile()) + Expect(layer).To(test.HaveOverrideSharedEnvironment("NODE_PATH", layer.Root)) + + Expect(filepath.Join(factory.Build.Application.Root, "node_gems")).NotTo(BeADirectory()) + }) + }) + }) + }) +} diff --git a/gems/mocks_test.go b/gems/mocks_test.go new file mode 100644 index 00000000..490520d4 --- /dev/null +++ b/gems/mocks_test.go @@ -0,0 +1,45 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: gems.go + +// Package gems_test is a generated GoMock package. +package gems_test + +import ( + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockPackageManager is a mock of PackageManager interface +type MockPackageManager struct { + ctrl *gomock.Controller + recorder *MockPackageManagerMockRecorder +} + +// MockPackageManagerMockRecorder is the mock recorder for MockPackageManager +type MockPackageManagerMockRecorder struct { + mock *MockPackageManager +} + +// NewMockPackageManager creates a new mock instance +func NewMockPackageManager(ctrl *gomock.Controller) *MockPackageManager { + mock := &MockPackageManager{ctrl: ctrl} + mock.recorder = &MockPackageManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockPackageManager) EXPECT() *MockPackageManagerMockRecorder { + return m.recorder +} + +// Install mocks base method +func (m *MockPackageManager) Install(location string) error { + ret := m.ctrl.Call(m, "Install", location) + ret0, _ := ret[0].(error) + return ret0 +} + +// Install indicates an expected call of Install +func (mr *MockPackageManagerMockRecorder) Install(location interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Install", reflect.TypeOf((*MockPackageManager)(nil).Install), location) +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..ddd7194e --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module bundler-cnb + +require ( + github.com/buildpack/libbuildpack v1.8.0 + github.com/cloudfoundry/libcfbuildpack v1.37.0 + github.com/golang/mock v1.2.0 + github.com/onsi/gomega v1.4.3 + github.com/sclevine/spec v1.2.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..fdcb45c4 --- /dev/null +++ b/go.sum @@ -0,0 +1,57 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= +github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/buildpack/libbuildpack v1.8.0 h1:m7Pxgw9NkNXV34yh0EHAX9d1Pdnbl6AW+eH4ARDsb1I= +github.com/buildpack/libbuildpack v1.8.0/go.mod h1:5YC4KNbppjwmsX561s0rS+4rtuJA++AnzUvcKc6lexc= +github.com/cloudfoundry/libcfbuildpack v1.37.0 h1:/jeSWneCY2QD0n91OgaEHJrv9FUTZV+uoorSAdPg0Ik= +github.com/cloudfoundry/libcfbuildpack v1.37.0/go.mod h1:WqW/cPLKqNnxhBt0JLTCQGpcKlFfC1T6vLHg0gNRtAA= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/sclevine/spec v1.2.0 h1:1Jwdf9jSfDl9NVmt8ndHqbTZ7XCCPbh1jI3hkDBHVYA= +github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190102155601-82a175fd1598 h1:S8GOgffXV1X3fpVG442QRfWOt0iFl79eHJ7OPt725bo= +golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/ruby/ruby.go b/ruby/ruby.go new file mode 100644 index 00000000..a67a4805 --- /dev/null +++ b/ruby/ruby.go @@ -0,0 +1,59 @@ +package ruby + +import ( + "encoding/json" + "fmt" + "os/exec" + "path/filepath" + "strings" +) + +const Dependency = "ruby" + +func GetRubyVersion(gemFile string) (version string, err error) { + constraints, err := Version(gemFile) + if err != nil { + return "", err + } + + for index, str := range constraints { + constraints[index] = fmt.Sprintf("'%s'",str) + } + + return strings.Join(constraints, ","), nil +} + +func Version(gemfile string) ([]string, error) { + dir := filepath.Dir(gemfile) + code := ` +stdout, $stdout = $stdout, $stderr +begin + def data() + return Bundler::Dsl.evaluate("Gemfile", 'Gemfile.lock', {}).ruby_version.engine_versions + end + out = data() + stdout.puts({error:nil, data:out}.to_json) +rescue => e + stdout.puts({error:e.to_s, data:nil}.to_json) +end +` + + cmd := exec.Command("ruby", "-rjson", "-rbundler", "-e", code) + cmd.Dir = dir + body, err := cmd.Output() + if err != nil { + fmt.Println(body) + return []string{}, err + } + output := struct { + Error string `json:"error"` + Data []string `json:"data"` + }{} + if err := json.Unmarshal(body, &output); err != nil { + return []string{}, err + } + if output.Error != "" { + return []string{}, fmt.Errorf("Running ruby: %s", output.Error) + } + return output.Data, nil +} diff --git a/scripts/all-tests.sh b/scripts/all-tests.sh new file mode 100755 index 00000000..e924026c --- /dev/null +++ b/scripts/all-tests.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$( dirname "${BASH_SOURCE[0]}" )/.." +./scripts/unit.sh && ./scripts/integration.sh + diff --git a/scripts/install_tools.sh b/scripts/install_tools.sh new file mode 100755 index 00000000..7783fb8a --- /dev/null +++ b/scripts/install_tools.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +PACK_VERSION="0.0.8" + +install_pack() { + OS=$(uname -s) + + if [[ $OS == "Darwin" ]]; then + OS="macos" + elif [[ $OS == "Linux" ]]; then + OS="linux" + else + echo "Unsupported operating system" + exit 1 + fi + + PACK_ARTIFACT=pack-$PACK_VERSION-$OS.tar.gz + + wget https://github.com/buildpack/pack/releases/download/v$PACK_VERSION/$PACK_ARTIFACT + tar xzvf $PACK_ARTIFACT -C .bin + rm $PACK_ARTIFACT +} + + +cd "$( dirname "${BASH_SOURCE[0]}" )/.." + +mkdir -p .bin +export PATH=$(pwd)/.bin:$PATH + +if [[ ! -f .bin/pack ]]; then + install_pack +elif [[ $(.bin/pack version | cut -d ' ' -f 2) != "v$PACK_VERSION" ]]; then + rm .bin/pack + install_pack +fi diff --git a/scripts/integration.sh b/scripts/integration.sh new file mode 100755 index 00000000..8f610be4 --- /dev/null +++ b/scripts/integration.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$( dirname "${BASH_SOURCE[0]}" )/.." +source ./scripts/install_tools.sh + +# TODO: change default to `cfbuildpacks/cflinuxfs3-cnb-experimental:build` when pack cli can use it +export CNB_BUILD_IMAGE=${CNB_BUILD_IMAGE:-packs/samples:v3alpha2} + +# TODO: change default to `cfbuildpacks/cflinuxfs3-cnb-experimental:run` when pack cli can use it +export CNB_RUN_IMAGE=${CNB_RUN_IMAGE:-packs/run:v3alpha2} + +# Always pull latest images +# Most helpful for local testing consistency with CI (which would already pull the latest) +docker pull $CNB_BUILD_IMAGE +docker pull $CNB_RUN_IMAGE + +echo "Run Buildpack Runtime Integration Tests" +go test ./integration/... -v -run Integration diff --git a/scripts/package.sh b/scripts/package.sh new file mode 100755 index 00000000..55a15078 --- /dev/null +++ b/scripts/package.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +TARGET_OS=${1:-linux} + +cd "$( dirname "${BASH_SOURCE[0]}" )/.." + +echo "Target OS is $TARGET_OS" +echo -n "Creating buildpack directory..." +bp_dir=/tmp/"${PWD##*/}"_$(openssl rand -hex 12) +mkdir $bp_dir +echo "done" + +echo -n "Copying buildpack.toml..." +cp buildpack.toml $bp_dir/buildpack.toml +echo "done" + +# TODO: Update list of built binaries as they are written +for b in detect build; do + echo -n "Building $b..." + GOOS=$TARGET_OS go build -o $bp_dir/bin/$b ./cmd/$b + echo "done" +done +echo "Buildpack packaged into: $bp_dir" diff --git a/scripts/unit.sh b/scripts/unit.sh new file mode 100755 index 00000000..a8f86414 --- /dev/null +++ b/scripts/unit.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$( dirname "${BASH_SOURCE[0]}" )/.." + +echo "Run Buildpack Unit Tests" +go test ./... -v -run Unit + +