• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

navarasu / serverless-ruby-layer / f16e1b5a-ff33-4235-9643-fa56a95c890c

07 Oct 2023 09:32PM UTC coverage: 84.588% (-5.7%) from 90.323%
f16e1b5a-ff33-4235-9643-fa56a95c890c

push

circleci

web-flow
Added Ruby 3.2 Support

70 of 92 branches covered (0.0%)

Branch coverage included in aggregate %.

166 of 187 relevant lines covered (88.77%)

121.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.9
/lib/bundle.js
1
const { spawnSync } = require('child_process');
8✔
2
const path = require('path');
8✔
3
const fs = require('fs-extra');
8✔
4
const rimraf = require('rimraf');
8✔
5
const Promise = require('bluebird');
8✔
6
var JSZip = require('jszip');
8✔
7
Promise.promisifyAll(fs);
8✔
8

9
function runCommand(cmd,args,options,cli) {
10
  const ps = spawnSync(cmd, args,options);
73✔
11
  if (ps.stdout && process.env.SLS_DEBUG){
73✔
12
    cli.log(ps.stdout.toString())
26✔
13
  }
14
  if (ps.stderr){
73✔
15
    cli.log(ps.stderr.toString())
5✔
16
  }
17
  if (ps.error && ps.error.code == 'ENOENT'){
73!
18
    return ps;
×
19
  }
20
  if (ps.error) {
73!
21
    throw new Error(ps.error);
×
22
  } else if (ps.status !== 0) {
73!
23
    throw new Error(ps.stderr);
×
24
  }
25
  return ps;
73✔
26
}
27

28
function docker(args, options,cli){
29
    return runCommand("docker",args,options,cli)
49✔
30
}
31

32
function cleanBuild(){
33
  this.cli = this.serverless.cli
8✔
34
  this.cli.log("Clearing previous build ruby layer build")
8✔
35
  this.ruby_layer = path.join(this.servicePath,'.serverless','ruby_layer')
8✔
36
  if (fs.pathExistsSync(this.ruby_layer)){
8!
37
    rimraf.sync(this.ruby_layer)
×
38
  }
39
}
40

41
function bundleInstall(){
42
  this.debug = process.env.SLS_DEBUG;
8✔
43
  const gem_file_path = path.join(this.servicePath,'Gemfile')
8✔
44
  if (!fs.pathExistsSync(gem_file_path)){
8!
45
    throw new Error("No Gemfile found in the path "+gem_file_path+"\n Please add a Gemfile in the path");
×
46
  }
47

48
  fs.copySync(path.join(this.servicePath,'Gemfile'), path.join(this.ruby_layer,'Gemfile') )
8✔
49
  const gem_file_lock_path = path.join(this.servicePath,'Gemfile.lock')
8✔
50
  if (this.use_gemfile_lock){
8✔
51
    fs.copySync(gem_file_lock_path, path.join(this.ruby_layer,'Gemfile.lock') )
1✔
52
  }
53

54
  this.build_path = path.join(this.ruby_layer, 'build')
8✔
55
  fs.mkdirSync(this.build_path)
8✔
56
  const options = {cwd : this.ruby_layer, encoding : 'utf8'}
8✔
57
  if (this.options.use_docker) {
8✔
58
    docker_name = 'public.ecr.aws/sam/build-'+this.serverless.service.provider.runtime+':latest-x86_64'
3✔
59
    ps=docker(['version'], options,this.cli)
3✔
60
    if (ps.error && ps.error.code === 'ENOENT') {
3!
61
      throw new Error('docker command not found. Please install docker https://www.docker.com/products/docker-desktop');
×
62
    }
63
    let buildDocker = false;
3✔
64
    if (this.options.docker_file) {
3✔
65
      const docker_file_path = path.join(this.servicePath, this.options.docker_file)
1✔
66
      if (!fs.pathExistsSync(docker_file_path)){
1!
67
          throw new Error("No Dockerfile found in the path "+docker_file_path);
×
68
      }
69

70
      fs.copySync(docker_file_path,
1✔
71
                  path.join(this.ruby_layer,'Dockerfile'))
72
      buildDocker = true
1✔
73
    }else if (this.options.docker_yums || this.options.environment) {
2✔
74
      docker_steps =['FROM public.ecr.aws/sam/build-'+this.serverless.service.provider.runtime+':latest-x86_64']
1✔
75
      if (this.options.docker_yums) {
1!
76
        this.options.docker_yums.forEach(function(package_name) {
1✔
77
          docker_steps.push('RUN yum install -y '+package_name)
1✔
78
        })
79
      }
80
      docker_steps.push('RUN gem update bundler')
1✔
81
      if(this.options.environment){
1!
82
        this.options.environment.forEach(function(env_arg) {
×
83
          env_arg_name = env_arg.split('=')[0]
×
84
          docker_steps.push('ARG ' + env_arg_name)
×
85
          docker_steps.push('ENV ' + env_arg_name + '=${' + env_arg_name + '}')
×
86
        })
87
      }
88
      docker_steps.push('CMD "/bin/bash"')
1✔
89
      fs.writeFileSync(path.join(this.ruby_layer,'Dockerfile'),docker_steps.join('\n'))
1✔
90
      buildDocker = true
1✔
91
    }
92
    if (buildDocker) {
3✔
93
      this.cli.log("Building docker for bundle install")
2✔
94
      docker_name ='ruby-layer:docker'
2✔
95
      docker_args = ['build', '-t', docker_name, '-f', 'Dockerfile']
2✔
96
      if(this.options.environment){
2!
97
        this.options.environment.forEach(function(env_arg) {
×
98
          docker_args.push('--build-arg')
×
99
          docker_args.push(env_arg)
×
100
        })
101
      }
102
      docker_args.push('.')
2✔
103
      docker(docker_args, options,this.cli)
2✔
104
    }
105
    let docker_id = Date.now().toString(36)  + '-'+Math.random().toString(36).slice(2, 15)
3✔
106
    ps=docker(['run','-it','--name',docker_id,'-d', docker_name,'/bin/bash'], options,this.cli)
3✔
107
    container_id = ps.stdout.toString().trim()
3✔
108
    console.log(container_id)
3✔
109
    if (this.options.native_libs) {
3✔
110
      this.cli.log("Packing the native libraries from the specified path")
2✔
111
      const lib_path = path.join(this.build_path,'lib')
2✔
112
      fs.mkdirSync(lib_path)
2✔
113
      this.options.native_libs.forEach(lib_to_copy => {
2✔
114
        ps=docker(['cp','-L', container_id+':'+lib_to_copy, lib_path],options,this.cli)
14✔
115
      })
116
    }
117
    this.cli.log("Installing gem using docker bundler")
3✔
118
    this.docker_gem_path = '/var/gem_build'
3✔
119
    docker(['exec', docker_id, 'mkdir', this.docker_gem_path], options, this.cli)
3✔
120
    docker(['cp', '-L', 'Gemfile', container_id+':'+this.docker_gem_path], options,this.cli)
3✔
121
    if (this.use_gemfile_lock){
3!
122
      docker(['cp', '-L', 'Gemfile.lock', container_id+':'+this.docker_gem_path], options,this.cli)
×
123
      const data = fs.readFileSync(gem_file_lock_path).toString().match(/BUNDLED WITH[\r\n]+([^\r\n]+)/)
×
124
      docker(['exec','-w',this.docker_gem_path, docker_id].concat(['gem','install','bundler:'+ data[data.length - 1].trim()]),options,this.cli)
×
125
    }
126
    ps = docker(['exec','-w',this.docker_gem_path, docker_id].concat(['bundle','-v']),options,this.cli)
3✔
127
    bundle_args = setBundleConfig(ps.stdout.toString(),(args) =>{
3✔
128
      docker(['exec','-w',this.docker_gem_path, docker_id, 'bundle','config', 'set', '--local'].concat(args), options, this.cli)
6✔
129
    })
130
    docker(['exec','-w',this.docker_gem_path, docker_id].concat(bundle_args),options,this.cli)
3✔
131
    docker(['cp', '-L', container_id+':'+this.docker_gem_path+'/build/ruby', this.build_path], options, this.cli)
3✔
132
    docker(['stop', container_id], options, this.cli)
3✔
133
    docker(['rm',docker_id], options, this.cli)
3✔
134
  } else {
135
    ps = runCommand("bundle",['-v'], options, this.cli)
5✔
136
     bundle_version = ps.stdout.toString()
5✔
137
    if (ps.error && ps.error.code === 'ENOENT') {
5!
138
       throw new Error('bundle command not found in local. Please install ruby. https://www.ruby-lang.org/en/downloads/');
×
139
    }
140
    bundle_args = setBundleConfig(bundle_version,(args) =>{
5✔
141
      runCommand('bundle', ['config', 'set', '--local'].concat(args),options,this.cli)
10✔
142
    })
143
    this.cli.log("Installing gem using local bundler")
5✔
144
    if (this.debug) {
5✔
145
      this.cli.log("Ruby layer Path: \n "+ this.ruby_layer)
4✔
146
      ps = runCommand("ruby",['--version'], options, this.cli)
4✔
147
      this.cli.log("Ruby version: "+ ps.stdout.toString().trim())
4✔
148
      this.cli.log("Bundler version: "+ bundle_version)
4✔
149
      this.cli.log(bundle_args.join(" "))
4✔
150
    }
151
    runCommand(bundle_args[0],bundle_args.slice(1,bundle_args.length),options,this.cli)
5✔
152
  }
153
}
154

155
function setBundleConfig(version, config){
156
  bundle_args = ['bundle', 'install']
8✔
157
  version = version.trim().match(/\d+\.\d+/g)
8✔
158
  console.log(version)
8✔
159
  if (version.length > 0 &&  !isNaN(parseFloat(version[0])) && parseFloat(version[0]) >= 2.1) {
8!
160
    [['path','build'] ,['without','test', 'development']].forEach(config)
8✔
161
    return bundle_args
8✔
162
  }
163
  return bundle_args.concat(['--path=build', '--without', 'test', 'development'])
×
164

165
}
166

167

168
function zipDir(folder_path,targetPath,zipOptions){
169
  zip = new JSZip()
8✔
170
  return addDirtoZip(zip, folder_path)
8✔
171
          .then(() => {
172
            return new Promise(resolve =>
8✔
173
                        zip.generateNodeStream(zipOptions)
8✔
174
                           .pipe(fs.createWriteStream(targetPath))
175
                           .on('finish', resolve))
176
                           .then(() => null)});
8✔
177
}
178

179
function addDirtoZip(zip, dir_path){
180
  const dir_path_norm = path.normalize(dir_path)
601✔
181
  return fs.readdirAsync(dir_path_norm)
601✔
182
           .map(file_name => {
183
              return addFiletoZip(zip, dir_path_norm, file_name)
2,940✔
184
            });
185
}
186

187
function addFiletoZip(zip, dir_path, file_name){
188
  const filePath = path.join(dir_path, file_name)
2,940✔
189
  return fs.statAsync(filePath)
2,940✔
190
          .then(stat => {
191
            if (stat.isDirectory()){
2,940✔
192
              if (new RegExp('ruby/[^/]*/cache$').test(filePath)){
601✔
193
                return undefined;
8✔
194
              }
195
              return addDirtoZip(zip.folder(file_name), filePath);
593✔
196
            } else {
197
                const file_option = { date: stat.mtime, unixPermissions: stat.mode };
2,339✔
198
                return fs.readFileAsync(filePath)
2,339✔
199
                  .then(data => zip.file(file_name, data, file_option));
2,339✔
200
              }
201
          });
202
}
203

204
function zipBundleFolder() {
205
  this.gem_folder= fs.readdirSync(path.join(this.build_path,'ruby'))[0]
8✔
206
  const platform = process.platform == 'win32' ? 'DOS' : 'UNIX'
8!
207
  zipping_message = "Zipping the gemfiles"
8✔
208
  if (this.options.native_libs) {
8✔
209
    zipping_message+=" and native libs"
2✔
210
  }
211
  this.gemLayer_zip_path = path.join(this.ruby_layer, 'gemLayer.zip')
8✔
212
  this.cli.log(zipping_message+ ' to '+ this.gemLayer_zip_path)
8✔
213
  return zipDir(this.build_path, this.gemLayer_zip_path,
8✔
214
                { platform: platform, compression: 'DEFLATE',
215
                compressionOptions: { level: 9 }});
216
}
217

218
function excludePackage(){
219
  if (!this.serverless.service.package){
8!
220
    this.serverless.service.package = Object.assign({})
×
221
  }
222
  if (!this.serverless.service.package["exclude"]){
8!
223
    this.serverless.service.package["exclude"] = Object.assign([])
8✔
224
  }
225
  this.serverless.service.package["exclude"].push("node_modules/**", "package-lock.json", "package.json",
8✔
226
                                                  "vendor/**")
227
  this.use_gemfile_lock = !this.options.ignore_gemfile_lock && fs.pathExistsSync(path.join(this.servicePath,'Gemfile.lock'))
8✔
228

229
  if (!this.use_gemfile_lock) {
8✔
230
    this.serverless.service.package["exclude"].push("Gemfile", "Gemfile.lock")
7✔
231
  }
232
  if (this.options.docker_file) {
8✔
233
    this.serverless.service.package["exclude"].push(this.options.docker_file)
1✔
234
  }
235
}
236

237
function configureLayer() {
238
  this.cli.log("Configuring Layer and GEM_PATH to the functions")
8✔
239
  if(this.debug){
8✔
240
    this.cli.log("GEM_PATH:" + "/opt/ruby/"+this.gem_folder)
6✔
241
    this.cli.log("Zip Path:" + this.gemLayer_zip_path )
6✔
242
  }
243
  if (!this.serverless.service.layers) {
8!
244
    this.serverless.service.layers = {};
×
245
  }
246
  this.serverless.service.layers['gem'] = Object.assign(
8✔
247
    {
248
      package:  {artifact: this.gemLayer_zip_path },
249
      name: `${
250
        this.serverless.service.service
251
      }-${this.serverless.providers.aws.getStage()}-ruby-bundle`,
252
      description:
253
        'Ruby gem generated by serverless-ruby-bundler',
254
      compatibleRuntimes: [this.serverless.service.provider.runtime]
255
    },
256
    this.options.layer
257
  );
258
  let functions_to_add = Object.keys(this.serverless.service.functions)
8✔
259

260
  if (this.options.include_functions){
8✔
261
    functions_to_add = this.options.include_functions
1✔
262
  }else if (this.options.exclude_functions) {
7✔
263
    functions_to_add = functions_to_add.filter(n => !this.options.exclude_functions.includes(n))
3✔
264
  }
265

266
  functions_to_add.forEach(funcName => {
8✔
267
    if(this.debug){
10✔
268
      this.cli.log("Configuring Layer for function: " + funcName)
8✔
269
    }
270
    const function_ = this.serverless.service.getFunction(funcName)
10✔
271
    if (!function_.environment){
10!
272
      function_.environment={}
10✔
273
    }
274
    function_.environment["GEM_PATH"]="/opt/ruby/"+this.gem_folder
10✔
275

276
    if (!function_.layers){
10!
277
      function_.layers=[]
10✔
278
    }
279
    function_.layers.push({"Ref":"GemLambdaLayer"})
10✔
280
  })
281
  return Promise.resolve();
8✔
282
}
283

284
function bundleGems()  {
285
  return Promise.bind(this)
8✔
286
          .then(cleanBuild)
287
          .then(bundleInstall)
288
          .then(zipBundleFolder)
289
          .then(configureLayer)
290
}
291

292
module.exports = { bundleGems, excludePackage };
8✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc