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

Commit

Permalink
feat: build Rust functions from source (#587)
Browse files Browse the repository at this point in the history
* feat: build Go functions from source

* feat: build Rust functions

* feat: add support for Rust functions

* chore: add tests

* chore: remove unused test fixture

* chore: remove outdated comment

* chore: update comments

* feat: check for Rust toolchain

* chore: add test
  • Loading branch information
eduardoboucas committed Jul 26, 2021
1 parent bd7206e commit 5d48d64
Show file tree
Hide file tree
Showing 13 changed files with 1,017 additions and 48 deletions.
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"resolve": "^2.0.0-next.1",
"semver": "^7.0.0",
"tmp-promise": "^3.0.2",
"toml": "^3.0.0",
"unixify": "^1.0.0",
"yargs": "^16.0.0"
},
Expand Down
1 change: 1 addition & 0 deletions src/feature_flags.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const { env } = require('process')
// List of supported flags and their default value.
const FLAGS = {
buildGoSource: Boolean(env.NETLIFY_EXPERIMENTAL_BUILD_GO_SOURCE),
buildRustSource: Boolean(env.NETLIFY_EXPERIMENTAL_BUILD_RUST_SOURCE),
}

const getFlags = (input = {}, flags = FLAGS) =>
Expand Down
4 changes: 2 additions & 2 deletions src/runtimes/go/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ const detectGoFunction = async ({ fsCache, path }) => {
const stat = await cachedLstat(fsCache, path)

if (!stat.isDirectory()) {
return false
return
}

const directoryName = basename(path)
const files = await cachedReaddir(fsCache, path)
const mainFileName = [`${directoryName}.go`, 'main.go'].find((name) => files.includes(name))

if (mainFileName === undefined) {
return false
return
}

return mainFileName
Expand Down
46 changes: 0 additions & 46 deletions src/runtimes/rust.js

This file was deleted.

67 changes: 67 additions & 0 deletions src/runtimes/rust/builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const { readFile } = require('fs')
const { basename, join } = require('path')
const { promisify } = require('util')

const pReadFile = promisify(readFile)

const tmp = require('tmp-promise')
const toml = require('toml')

const { lstat } = require('../../utils/fs')
const { runCommand } = require('../../utils/shell')

const { BUILD_TARGET, MANIFEST_NAME } = require('./constants')

const build = async ({ srcDir }) => {
// We compile the binary to a temporary directory so that we don't pollute
// the user's functions directory.
const { path: targetDirectory } = await tmp.dir()
const functionName = basename(srcDir)

try {
await runCommand('cargo', ['build', '--target', BUILD_TARGET, '--release'], {
cwd: srcDir,
env: {
CARGO_TARGET_DIR: targetDirectory,
},
})
} catch (error) {
const hasToolchain = await checkRustToolchain()

if (!hasToolchain) {
throw new Error(
'There is no Rust toolchain installed. Visit https://ntl.fyi/missing-rust-toolchain for more information.',
)
}

console.error(`Could not compile Rust function ${functionName}:\n`)

throw error
}

// By default, the binary will have the same name as the crate and there's no
// way to override it (https://github.com/rust-lang/cargo/issues/1706). We
// must extract the crate name from the manifest and use it to form the path
// to the binary.
const manifest = await pReadFile(join(srcDir, MANIFEST_NAME))
const { package } = toml.parse(manifest)
const binaryPath = join(targetDirectory, BUILD_TARGET, 'release', package.name)
const stat = await lstat(binaryPath)

return {
path: binaryPath,
stat,
}
}

const checkRustToolchain = async () => {
try {
await runCommand('cargo', ['-V'])

return true
} catch (_) {
return false
}
}

module.exports = { build }
4 changes: 4 additions & 0 deletions src/runtimes/rust/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const BUILD_TARGET = 'x86_64-unknown-linux-musl'
const MANIFEST_NAME = 'Cargo.toml'

module.exports = { BUILD_TARGET, MANIFEST_NAME }
112 changes: 112 additions & 0 deletions src/runtimes/rust/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
const { join, extname, dirname, basename } = require('path')

const { RUNTIME_RUST } = require('../../utils/consts')
const { cachedLstat, cachedReaddir } = require('../../utils/fs')
const { zipBinary } = require('../../zip_binary')
const { detectBinaryRuntime } = require('../detect_runtime')

const { build } = require('./builder')
const { MANIFEST_NAME } = require('./constants')

const detectRustFunction = async ({ fsCache, path }) => {
const stat = await cachedLstat(fsCache, path)

if (!stat.isDirectory()) {
return
}

const files = await cachedReaddir(fsCache, path)
const hasCargoManifest = files.includes(MANIFEST_NAME)

if (!hasCargoManifest) {
return
}

const mainFilePath = join(path, 'src', 'main.rs')

try {
const mainFile = await cachedLstat(fsCache, mainFilePath)

if (mainFile.isFile()) {
return mainFilePath
}
} catch (_) {
// no-op
}
}

const findFunctionsInPaths = async function ({ featureFlags, fsCache, paths }) {
const functions = await Promise.all(
paths.map(async (path) => {
const runtime = await detectBinaryRuntime({ fsCache, path })

if (runtime === RUNTIME_RUST) {
return processBinary({ fsCache, path })
}

if (featureFlags.buildRustSource !== true) {
return
}

const rustSourceFile = await detectRustFunction({ fsCache, path })

if (rustSourceFile) {
return processSource({ fsCache, mainFile: rustSourceFile, path })
}
}),
)

return functions.filter(Boolean)
}

const processBinary = async ({ fsCache, path }) => {
const stat = await cachedLstat(fsCache, path)
const name = basename(path, extname(path))

return {
mainFile: path,
name,
srcDir: dirname(path),
srcPath: path,
stat,
}
}

const processSource = ({ mainFile, path }) => {
const functionName = basename(path)

return {
mainFile,
name: functionName,
srcDir: path,
srcPath: path,
}
}

// The name of the binary inside the zip file must always be `bootstrap`
// because they include the Lambda runtime, and that's the name that AWS
// expects for those kind of functions.
const zipFunction = async function ({ config, destFolder, filename, mainFile, runtime, srcDir, srcPath, stat }) {
const destPath = join(destFolder, `${filename}.zip`)
const isSource = extname(mainFile) === '.rs'
const zipOptions = {
destPath,
filename: 'bootstrap',
runtime,
}

// If we're building from source, we first need to build the source and zip
// the resulting binary. Otherwise, we're dealing with a binary so we zip it
// directly.
if (isSource) {
const { path: binaryPath, stat: binaryStat } = await build({ srcDir })

await zipBinary({ ...zipOptions, srcPath: binaryPath, stat: binaryStat })
} else {
await zipBinary({ ...zipOptions, srcPath, stat })
}

return { config, path: destPath }
}

module.exports = { findFunctionsInPaths, name: RUNTIME_RUST, zipFunction }
1 change: 1 addition & 0 deletions tests/fixtures/rust-source/rust-func-1/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target

1 comment on commit 5d48d64

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⏱ Benchmark results

largeDepsEsbuild: 12.4s

largeDepsZisi: 1m 2.1s

Please sign in to comment.