Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use a fully static seshat build #631

Merged
merged 12 commits into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions .github/workflows/build_linux.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ jobs:
- name: Prepare for static sqlcipher build
if: inputs.sqlcipher == 'static'
run: |
echo "SQLCIPHER_STATIC=1" >> $GITHUB_ENV
echo "SQLCIPHER_BUNDLED=1" >> $GITHUB_ENV

# Ideally the docker image would be ready for cross-compilation but libsqlcipher-dev is not Multi-Arch compatible
# https://unix.stackexchange.com/a/349359
Expand Down Expand Up @@ -150,17 +150,13 @@ jobs:
LIBS=$(readelf -d dist/**/*.node | grep NEEDED)
echo "$LIBS"

if [ "$SQLCIPHER_STATIC" == "1" ]; then
if grep -q "libsqlcipher.so.0" <<< "$LIBS" ; then
exit 2
fi
set +x
assert_contains_string() { [[ "$1" == *"$2"* ]]; }
! assert_contains_string "$LIBS" "libcrypto.so.1.1"
if [ "$SQLCIPHER_BUNDLED" == "1" ]; then
! assert_contains_string "$LIBS" "libsqlcipher.so.0"
else
if grep -q "libcrypto.so.1.1" <<< "$LIBS" ; then
exit 3
fi
if ! grep -q "libsqlcipher.so.0" <<< "$LIBS" ; then
exit 4
fi
assert_contains_string "$LIBS" "libsqlcipher.so.0"
fi
env:
ARCH: ${{ steps.config.outputs.arch }}
Expand Down
6 changes: 3 additions & 3 deletions docs/native-node-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ as usual using:

On Windows & macOS we always statically link libsqlcipher for it is not generally available.
On Linux by default we will use a system package, on debian & ubuntu this is `libsqlcipher0`,
but this is problematic for some other packages.
By including `SQLCIPHER_STATIC=1` in the build environment, the build scripts will statically link sqlcipher,
note that this will want a `libcrypto1.1` shared library available in the system.
but this is problematic for some other packages, and we found that it may crashes for unknown reasons.
By including `SQLCIPHER_BUNDLED=1` in the build environment, the build scripts will fully statically
link sqlcipher, including a static build of OpenSSL.

More info can be found at https://github.com/matrix-org/seshat/issues/102
and https://github.com/vector-im/element-web/issues/20926.
Expand Down
287 changes: 14 additions & 273 deletions hak/matrix-seshat/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,109 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import path from "path";
import childProcess from "child_process";
import { mkdirp } from "mkdirp";
import fsExtra from "fs-extra";

import HakEnv from "../../scripts/hak/hakEnv";
import { DependencyInfo } from "../../scripts/hak/dep";

type WinConfiguration =
| "VC-WIN32"
| "VC-WIN64A"
| "VC-WIN64-ARM"
| "VC-WIN64-CLANGASM-ARM"
| "VC-CLANG-WIN64-CLANGASM-ARM"
| "VC-WIN32-HYBRIDCRT"
| "VC-WIN64A-HYBRIDCRT";

export default async function (hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
if (hakEnv.isWin()) {
await buildOpenSslWin(hakEnv, moduleInfo);
await buildSqlCipherWin(hakEnv, moduleInfo);
} else if (hakEnv.wantsStaticSqlCipherUnix()) {
await buildSqlCipherUnix(hakEnv, moduleInfo);
}
await buildMatrixSeshat(hakEnv, moduleInfo);
}

async function buildOpenSslWin(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
const version = moduleInfo.cfg.dependencies.openssl;
const openSslDir = path.join(moduleInfo.moduleTargetDotHakDir, `openssl-${version}`);
const env = hakEnv.makeGypEnv();

let openSslArch: WinConfiguration;
switch (hakEnv.getTargetArch()) {
case "x64":
openSslArch = "VC-WIN64A";
break;
case "ia32":
openSslArch = "VC-WIN32";
break;
case "arm64":
openSslArch = "VC-WIN64-ARM";
break;
if (!hakEnv.isHost()) {
env.CARGO_BUILD_TARGET = hakEnv.getTargetId();
}

console.log("Building openssl in " + openSslDir);
console.log("Running yarn install");
await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn(
"perl",
[
"Configure",
"--prefix=" + moduleInfo.depPrefix,
// sqlcipher only uses about a tiny part of openssl. We link statically
// so will only pull in the symbols we use, but we may as well turn off
// as much as possible to save on build time.
"no-afalgeng",
"no-capieng",
"no-cms",
"no-ct",
"no-deprecated",
"no-dgram",
"no-dso",
"no-ec",
"no-ec2m",
"no-gost",
"no-nextprotoneg",
"no-ocsp",
"no-sock",
"no-srp",
"no-srtp",
"no-tests",
"no-ssl",
"no-tls",
"no-dtls",
"no-shared",
"no-aria",
"no-camellia",
"no-cast",
"no-chacha",
"no-cmac",
"no-des",
"no-dh",
"no-dsa",
"no-ecdh",
"no-ecdsa",
"no-idea",
"no-md4",
"no-mdc2",
"no-ocb",
"no-poly1305",
"no-rc2",
"no-rc4",
"no-rmd160",
"no-scrypt",
"no-seed",
"no-siphash",
"no-sm2",
"no-sm3",
"no-sm4",
"no-whirlpool",
openSslArch,
],
"yarn" + (hakEnv.isWin() ? ".cmd" : ""),
["install"],
{
cwd: openSslDir,
cwd: moduleInfo.moduleBuildDir,
env,
shell: true,
stdio: "inherit",
},
);
Expand All @@ -125,194 +43,17 @@ async function buildOpenSslWin(hakEnv: HakEnv, moduleInfo: DependencyInfo): Prom
});
});

await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn("nmake", ["build_libs"], {
cwd: openSslDir,
stdio: "inherit",
});
proc.on("exit", (code) => {
code ? reject(code) : resolve();
});
});

await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn("nmake", ["install_dev"], {
cwd: openSslDir,
stdio: "inherit",
});
proc.on("exit", (code) => {
code ? reject(code) : resolve();
});
});
}

async function buildSqlCipherWin(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
const version = moduleInfo.cfg.dependencies.sqlcipher;
const sqlCipherDir = path.join(moduleInfo.moduleTargetDotHakDir, `sqlcipher-${version}`);
const buildDir = path.join(sqlCipherDir, "bld");

await mkdirp(buildDir);

await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn("nmake", ["/f", path.join("..", "Makefile.msc"), "libsqlite3.lib", "TOP=.."], {
cwd: buildDir,
stdio: "inherit",
env: Object.assign({}, process.env, {
CCOPTS: "-DSQLITE_HAS_CODEC -I" + path.join(moduleInfo.depPrefix, "include"),
LTLIBPATHS: "/LIBPATH:" + path.join(moduleInfo.depPrefix, "lib"),
LTLIBS: "libcrypto.lib",
}),
});
proc.on("exit", (code) => {
code ? reject(code) : resolve();
});
});

await fsExtra.copy(path.join(buildDir, "libsqlite3.lib"), path.join(moduleInfo.depPrefix, "lib", "sqlcipher.lib"));

await fsExtra.copy(path.join(buildDir, "sqlite3.h"), path.join(moduleInfo.depPrefix, "include", "sqlcipher.h"));
}

async function buildSqlCipherUnix(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
const version = moduleInfo.cfg.dependencies.sqlcipher;
const sqlCipherDir = path.join(moduleInfo.moduleTargetDotHakDir, `sqlcipher-${version}`);

const args = [
"--prefix=" + moduleInfo.depPrefix + "",
"--enable-tempstore=yes",
"--enable-shared=no",
"--enable-tcl=no",
];

if (hakEnv.isMac()) {
args.push("--with-crypto-lib=commoncrypto");
}

if (hakEnv.wantsStaticSqlCipherUnix()) {
args.push("--enable-tcl=no");

if (hakEnv.isLinux()) {
args.push("--with-pic=yes");
}
}

if (!hakEnv.isHost()) {
// In the nonsense world of `configure`, it is assumed you are building
// a compiler like `gcc`, so the `host` option actually means the target
// the build output runs on.
args.push(`--host=${hakEnv.getTargetId()}`);
}

const cflags = ["-DSQLITE_HAS_CODEC"];

// If the caller has specified CFLAGS then we shouldn't specify target
// as their compiler may be incompatible (gcc)
if (!hakEnv.isHost() && !process.env.CFLAGS) {
// `clang` uses more logical option naming.
cflags.push(`--target=${hakEnv.getTargetId()}`);
}

if (process.env.CFLAGS) cflags.unshift(process.env.CFLAGS);
args.push(`CFLAGS=${cflags.join(" ")}`);

const ldflags: string[] = [];

if (hakEnv.isMac()) {
ldflags.push("-framework Security");
ldflags.push("-framework Foundation");
}

if (ldflags.length) {
if (process.env.LDFLAGS) ldflags.unshift(process.env.LDFLAGS);
args.push(`LDFLAGS=${ldflags.join(" ")}`);
}

await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn(path.join(sqlCipherDir, "configure"), args, {
cwd: sqlCipherDir,
stdio: "inherit",
});
proc.on("exit", (code) => {
code ? reject(code) : resolve();
});
});

await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn("make", [], {
cwd: sqlCipherDir,
stdio: "inherit",
});
proc.on("exit", (code) => {
code ? reject(code) : resolve();
});
});

await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn("make", ["install"], {
cwd: sqlCipherDir,
stdio: "inherit",
});
proc.on("exit", (code) => {
code ? reject(code) : resolve();
});
});
}

async function buildMatrixSeshat(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
// seshat now uses n-api so we shouldn't need to specify a node version to
// build against, but it does seems to still need something in here, so leaving
// it for now: we should confirm how much of this it still actually needs.
const env = hakEnv.makeGypEnv();

if (!hakEnv.isLinux() || hakEnv.wantsStaticSqlCipherUnix()) {
Object.assign(env, {
SQLCIPHER_STATIC: 1,
SQLCIPHER_LIB_DIR: path.join(moduleInfo.depPrefix, "lib"),
SQLCIPHER_INCLUDE_DIR: path.join(moduleInfo.depPrefix, "include"),
});
}

if (hakEnv.isLinux() && hakEnv.wantsStaticSqlCipherUnix()) {
// Ensure Element uses the statically-linked seshat build, and prevent other applications
// from attempting to use this one. Detailed explanation:
//
// RUSTFLAGS
// An environment variable containing a list of arguments to pass to rustc.
// -Clink-arg=VALUE
// A rustc argument to pass a single argument to the linker.
// -Wl,
// gcc syntax to pass an argument (from gcc) to the linker (ld).
// -Bsymbolic:
// Prefer local/statically linked symbols over those in the environment.
// Prevent overriding native libraries by LD_PRELOAD etc.
// --exclude-libs ALL
// Prevent symbols from being exported by any archive libraries.
// Reduces output filesize and prevents being dynamically linked against.
env.RUSTFLAGS = "-Clink-arg=-Wl,-Bsymbolic -Clink-arg=-Wl,--exclude-libs,ALL";
}

if (hakEnv.isWin()) {
env.RUSTFLAGS = "-Ctarget-feature=+crt-static -Clink-args=libcrypto.lib";
// Note that in general, you can specify targets in Rust without having to have
// the matching toolchain, however for this, cargo gets confused when building
// the build scripts since they run on the host, but vcvarsall.bat sets the c
// compiler in the path to be the one for the target, so we just use the matching
// toolchain for the target architecture which makes everything happy.
env.RUSTUP_TOOLCHAIN = `stable-${hakEnv.getTargetId()}`;
}

if (!hakEnv.isHost()) {
env.CARGO_BUILD_TARGET = hakEnv.getTargetId();
}
const buildTarget = hakEnv.wantsStaticSqlCipher() ? "build-bundled" : "build";

console.log("Running neon with env", env);
console.log("Running yarn build");
await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn(
path.join(moduleInfo.nodeModuleBinDir, "neon" + (hakEnv.isWin() ? ".cmd" : "")),
["build", "--release"],
"yarn" + (hakEnv.isWin() ? ".cmd" : ""),
["run", buildTarget],
{
cwd: moduleInfo.moduleBuildDir,
env,
shell: true,
stdio: "inherit",
},
);
Expand Down
17 changes: 0 additions & 17 deletions hak/matrix-seshat/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,6 @@ import HakEnv from "../../scripts/hak/hakEnv";
import { DependencyInfo } from "../../scripts/hak/dep";

export default async function (hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
if (hakEnv.wantsStaticSqlCipher()) {
// of course tcl doesn't have a --version
await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn("tclsh", [], {
stdio: ["pipe", "ignore", "ignore"],
});
proc.on("exit", (code) => {
if (code !== 0) {
reject("Can't find tclsh - have you installed TCL?");
} else {
resolve();
}
});
proc.stdin.end();
});
}

const tools = [
["rustc", "--version"],
["python", "--version"], // node-gyp uses python for reasons beyond comprehension
Expand Down
Loading