From c9b23dcb756138c463ca63e8605becf4a7c7eec5 Mon Sep 17 00:00:00 2001 From: James Thomas Date: Tue, 5 Feb 2019 17:25:26 +0000 Subject: [PATCH] Updating Swift support with 4.2 runtime --- README.md | 42 ++++---- compile/functions/runtimes/swift.js | 51 +++++++--- compile/functions/runtimes/tests/swift.js | 119 +++++----------------- 3 files changed, 85 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index 798b575..c7d2dbe 100644 --- a/README.md +++ b/README.md @@ -393,40 +393,44 @@ func main(args: [String:Any]) -> [String:Any] { If you want to return an error message, return an object with an `error` property with the message. -## Writing Functions - Pre-Compiled Swift Binaries +### Codable Support + +Swift 4 runtimes support [Codable types](https://developer.apple.com/documentation/swift/codable) to handle the converting between JSON input parameters and response types to native Swift types. + +```swift +struct Employee: Codable { + let id: Int? + let name: String? +} +// codable main function +func main(input: Employee, respondWith: (Employee?, Error?) -> Void) -> Void { + // For simplicity, just passing same Employee instance forward + respondWith(input, nil) +} +``` + +### Pre-Compiled Swift Binaries OpenWhisk supports creating Swift actions from a pre-compiled binary. This reduces startup time for Swift actions by removing the need for a dynamic compilation step. -In the `serverless.yaml` file, the `handler` property can refer to the compiled binary file produced by the build. +In the `serverless.yaml` file, the `handler` property can refer to the zip file containing a binary file produced by the build. ```yaml functions: hello: - handler: .build/release/Hello + handler: action.zip ``` -This configuration will generate the deployment package for that function containing only this binary file. It will not include other local files, e.g. Swift source files. - -Pre-compiled Swift actions must be compatible with the platform runtime and architecture. There is an [open-source Swift package](https://packagecatalog.com/package/jthomas/OpenWhiskAction) (`OpenWhiskAction`) that handles wrapping functions within a shim to support runtime execution. +Compiling a single Swift file to a binary can be handled using this Docker command with the OpenWhisk Swift runtime image. `main.swift` is the file containing the swift code and `action.zip` is the zip archive produced. ``` -import OpenWhiskAction - -func hello(args: [String:Any]) -> [String:Any] { - if let name = args["name"] as? String { - return [ "greeting" : "Hello \(name)!" ] - } else { - return [ "greeting" : "Hello stranger!" ] - } -} - -OpenWhiskAction(main: hello) +docker run -i openwhisk/action-swift-v4.2 -compile main < main.swift > action.zip ``` -Binaries produced by the Swift build process must be generated for the correct platform architecture. This Docker command will compile Swift sources files using the relevant Swift environment. +Swift packages containing multiple source files with a package descriptor (`Package.swift` ) can be built using the following command. ``` -docker run --rm -it -v $(pwd):/swift-package openwhisk/action-swift-v3.1.1 bash -e -c "cd /swift-package && swift build -v -c release" +zip - -r * | docker run -i openwhisk/action-swift-v4.2 -compile main > action.zip ``` ## Writing Functions - Java diff --git a/compile/functions/runtimes/swift.js b/compile/functions/runtimes/swift.js index b83bc60..196b14c 100644 --- a/compile/functions/runtimes/swift.js +++ b/compile/functions/runtimes/swift.js @@ -1,5 +1,6 @@ 'use strict'; +const fs = require('fs-extra') const BaseRuntime = require('./base') const JSZip = require("jszip") @@ -11,29 +12,49 @@ class Swift extends BaseRuntime { } convertHandlerToPath (functionHandler) { - if (this.isBuildPath(functionHandler)) { + if (this.isZipFile(functionHandler)) { return functionHandler } return super.convertHandlerToPath(functionHandler) } - isBuildPath (path) { - return path.startsWith('.build/') + calculateFunctionMain(functionObject) { + if (this.isZipFile(functionObject.handler)) { + return 'main' + } + + return super.calculateFunctionMain(functionObject) + } + + isZipFile (path) { + return path.endsWith('.zip') + } + + readHandlerFile (path) { + const contents = fs.readFileSync(path) + const encoding = this.isZipFile(path) ? 'base64' : 'utf8' + return contents.toString(encoding) } - // Ensure zip package used to deploy action has the correct artifacts for the runtime. - // Swift source actions must have the function code in `main.swift`. - // Swift binary actions must have the binary as `./build/release/Action`. - processActionPackage (handlerFile, zip) { - return zip.file(handlerFile).async('nodebuffer').then(data => { - if (this.isBuildPath(handlerFile)) { - zip = new JSZip() - return zip.file('.build/release/Action', data) - } - zip.remove(handlerFile) - return zip.file('main.swift', data) - }) + exec (functionObject) { + const main = this.calculateFunctionMain(functionObject); + const kind = this.calculateKind(functionObject); + const handlerPath = this.convertHandlerToPath(functionObject.handler) + + if (!this.isValidFile(handlerPath)) { + throw new this.serverless.classes.Error(`Function handler (${handlerPath}) does not exist.`) + } + + const code = this.readHandlerFile(handlerPath) + const binary = this.isZipFile(handlerPath) + const exec = { main, kind, code, binary } + + if (functionObject.hasOwnProperty('image')) { + exec.image = functionObject.image + } + + return Promise.resolve(exec) } } diff --git a/compile/functions/runtimes/tests/swift.js b/compile/functions/runtimes/tests/swift.js index 18de98e..b624a72 100644 --- a/compile/functions/runtimes/tests/swift.js +++ b/compile/functions/runtimes/tests/swift.js @@ -56,30 +56,39 @@ describe('Swift', () => { }); describe('#exec()', () => { - it('should return swift exec definition', () => { + it('should return swift exec with source file handler', () => { const fileContents = 'some file contents'; const handler = 'handler.some_func'; - - const exec = { main: 'some_func', kind: 'swift:default', code: new Buffer(fileContents) }; - sandbox.stub(node, 'generateActionPackage', (functionObj) => { - expect(functionObj.handler).to.equal(handler); - return Promise.resolve(new Buffer(fileContents)); + node.isValidFile = () => true + sandbox.stub(fs, 'readFileSync', (path) => { + expect(path).to.equal('handler.swift'); + return Buffer.from(fileContents) }); + + const exec = { main: 'some_func', binary: false, kind: 'swift:default', code: fileContents }; return expect(node.exec({ handler, runtime: 'swift'})) .to.eventually.deep.equal(exec); }) - it('should return swift exec definition with custom image', () => { - const fileContents = 'some file contents'; - const handler = 'handler.some_func'; + it('should return swift exec with zip file handler', () => { + const handler = 'my_file.zip'; + node.isValidFile = () => true - const exec = { main: 'some_func', kind: 'blackbox', image: 'foo', code: new Buffer(fileContents) }; - sandbox.stub(node, 'generateActionPackage', (functionObj) => { - expect(functionObj.handler).to.equal(handler); - return Promise.resolve(new Buffer(fileContents)); - }); - return expect(node.exec({ handler, runtime: 'swift', image: 'foo' })) + const zip = new JSZip(); + const source = 'binary file contents' + zip.file("exec", source); + + return zip.generateAsync({type:"nodebuffer"}).then(zipped => { + sandbox.stub(fs, 'readFileSync', (path) => { + expect(path).to.equal(handler); + return zipped + }); + + const b64 = zipped.toString('base64') + const exec = { main: 'main', binary: true, kind: 'swift:default', code: b64 }; + return expect(node.exec({ handler, runtime: 'swift'})) .to.eventually.deep.equal(exec); + }) }) }); @@ -88,84 +97,8 @@ describe('Swift', () => { expect(node.convertHandlerToPath('file.func')).to.be.equal('file.swift') }) - it('should return file path for build binaries', () => { - expect(node.convertHandlerToPath('.build/release/Action')).to.be.equal('.build/release/Action') - }) - }) - - describe('#generateActionPackage()', () => { - it('should throw error for missing handler file', () => { - expect(() => node.generateActionPackage({handler: 'does_not_exist.main'})) - .to.throw(Error, 'Function handler (does_not_exist.swift) does not exist.'); - }) - - it('should read service artifact and add package.json for handler', () => { - node.serverless.service.package = {artifact: '/path/to/zip_file.zip'}; - node.isValidFile = () => true - const source = 'func main(args: [String:Any]) -> [String:Any] {\nreturn ["hello": "world"]\n}' - const zip = new JSZip(); - zip.file("handler.swift", source); - return zip.generateAsync({type:"nodebuffer"}).then(zipped => { - sandbox.stub(fs, 'readFile', (path, cb) => { - expect(path).to.equal('/path/to/zip_file.zip'); - cb(null, zipped); - }); - return node.generateActionPackage({handler: 'handler.main'}).then(data => { - return JSZip.loadAsync(new Buffer(data, 'base64')).then(zip => { - expect(zip.file("handler.swift")).to.be.equal(null) - return zip.file("main.swift").async("string").then(code => { - expect(code).to.be.equal(source) - }) - }) - }) - }); - }) - - it('should handle service artifact for individual function handler', () => { - const functionObj = {handler: 'handler.main', package: { artifact: '/path/to/zip_file.zip'}} - node.serverless.service.package = {individually: true}; - node.isValidFile = () => true - - const zip = new JSZip(); - const source = 'func main(args: [String:Any]) -> [String:Any] {\nreturn ["hello": "world"]\n}' - zip.file("handler.swift", source); - return zip.generateAsync({type:"nodebuffer"}).then(zipped => { - sandbox.stub(fs, 'readFile', (path, cb) => { - expect(path).to.equal('/path/to/zip_file.zip'); - cb(null, zipped); - }); - return node.generateActionPackage(functionObj).then(data => { - return JSZip.loadAsync(new Buffer(data, 'base64')).then(zip => { - expect(zip.file("handler.swift")).to.be.equal(null) - return zip.file("main.swift").async("string").then(code => { - expect(code).to.be.equal(source) - }) - }) - }) - }); + it('should return file path for zip files', () => { + expect(node.convertHandlerToPath('my_file.zip')).to.be.equal('my_file.zip') }) - - it('should create zip file with binary action', () => { - node.serverless.service.package = {artifact: '/path/to/zip_file.zip'}; - node.isValidFile = () => true - const zip = new JSZip(); - const source = 'binary file contents' - zip.file(".build/release/foo/bar", source); - return zip.generateAsync({type:"nodebuffer"}).then(zipped => { - sandbox.stub(fs, 'readFile', (path, cb) => { - expect(path).to.equal('/path/to/zip_file.zip'); - cb(null, zipped); - }); - return node.generateActionPackage({handler: '.build/release/foo/bar'}).then(data => { - return JSZip.loadAsync(new Buffer(data, 'base64')).then(zip => { - return zip.file(".build/release/Action").async("string").then(contents => { - expect(contents).to.be.equal(source) - }) - }) - }) - }); - }) - - }) });