Skip to content
This repository has been archived by the owner on Dec 9, 2024. It is now read-only.

Commit

Permalink
Updating Swift support with 4.2 runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
jthomas committed Feb 5, 2019
1 parent cc11269 commit c9b23dc
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 127 deletions.
42 changes: 23 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 36 additions & 15 deletions compile/functions/runtimes/swift.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

const fs = require('fs-extra')
const BaseRuntime = require('./base')
const JSZip = require("jszip")

Expand All @@ -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)
}
}

Expand Down
119 changes: 26 additions & 93 deletions compile/functions/runtimes/tests/swift.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
})
})
});

Expand All @@ -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)
})
})
})
});
})


})
});

0 comments on commit c9b23dc

Please sign in to comment.