stdio jsonrpc server npm package (#5332)

- [x] figgure out how to build the packages (that it installs native
optional package automatically)
- [X] Make the gluecode
- [x] expose both the lowerlevel api that desktop uses (~~send objects
and receive objects~~, getting path of rpc-server is enough)
  - [X] and the higher level api needed for bots (jsonrpc client)
  - [X] typescript types
- [x] automatically pick the right binary from npm or allow getting it
from env var, or give out an error (throw error)
- [x] find out how to dev locally (use local built core in dc desktop) -
there is the question of how to link the typescript client and the task
to add a search in the cargo target folder for a debug build or a
different way, find out some good flow that we can use and document for
dc desktop + locally built core development
- [x] build the packages in ci
- [x] fix that deltachat-rpc-server is not executable

postponed:
- [ ] publish from ci
   - [ ] add key/token to deploy to npm 

Closes #4694

## Related prs
- https://github.com/deltachat-bot/echo/pull/69
- https://github.com/deltachat/deltachat-desktop/pull/3567

---------

Co-authored-by: link2xt <link2xt@testrun.org>
This commit is contained in:
Simon Laux
2024-05-11 20:54:11 +02:00
committed by GitHub
parent 14aaab05b0
commit c375c03d8e
18 changed files with 695 additions and 2 deletions

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
import subprocess
from sys import argv
from os import path, makedirs, chdir
from shutil import copy
from src.make_package import write_package_json
# ensure correct working directory
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
if len(argv) < 2:
print("First argument should be target architecture as required by cargo")
exit(1)
target = argv[1].strip()
subprocess.run(
["cargo", "build", "--release", "-p", "deltachat-rpc-server", "--target", target],
check=True,
)
newpath = "platform_package"
if not path.exists(newpath):
makedirs(newpath)
# make new folder
platform_path = "platform_package/" + target
if not path.exists(platform_path):
makedirs(platform_path)
# copy binary it over
def binary_path(binary_name):
return "../../target/" + target + "/release/" + binary_name
my_binary_name = "deltachat-rpc-server"
if not path.isfile(binary_path("deltachat-rpc-server")):
my_binary_name = "deltachat-rpc-server.exe"
if not path.isfile(binary_path("deltachat-rpc-server.exe")):
print("Did not find the build")
exit(1)
my_binary_path = binary_path(my_binary_name)
copy(my_binary_path, platform_path + "/" + my_binary_name)
# make a package.json for it
write_package_json(platform_path, target, my_binary_name)

View File

@@ -0,0 +1,31 @@
# This script is for making a version of the npm packet that you can install locally
import subprocess
from sys import argv
from os import path, makedirs, chdir
import re
import json
import tomllib
from shutil import copy, rmtree
# ensure correct working directory
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
# get host target with "rustc -vV"
output = subprocess.run(["rustc", "-vV"], capture_output=True)
host_target = re.search('host: ([-\\w]*)', output.stdout.decode("utf-8")).group(1)
print("host target to build for is:", host_target)
# clean platform_package folder
newpath = r'platform_package'
if not path.exists(newpath):
makedirs(newpath)
else:
rmtree(path.join(path.dirname(path.abspath(__file__)), "../platform_package/"))
makedirs(newpath)
# run build_platform_package.py with the host's target to build it
subprocess.run(["python", "scripts/build_platform_package.py", host_target], capture_output=False, check=True)
# run update_optional_dependencies_and_version.js to adjust the package / make it installable locally
subprocess.run(["node", "scripts/update_optional_dependencies_and_version.js", "--local"], capture_output=False, check=True)

View File

@@ -0,0 +1,46 @@
import subprocess
from sys import argv
from os import path, makedirs, chdir, chmod, stat
import json
from shutil import copy
from src.make_package import write_package_json
# ensure correct working directory
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
if len(argv) < 3:
print("First argument should be target architecture as required by cargo")
print("Second argument should be the location of th built binary (binary_path)")
exit(1)
target = argv[1].strip()
binary_path = argv[2].strip()
output = subprocess.run(["rustc","--print","target-list"], capture_output=True, check=True)
available_targets = output.stdout.decode("utf-8")
if available_targets.find(target) == -1:
print("target", target, "is not known / not valid")
exit(1)
newpath = r'platform_package'
if not path.exists(newpath):
makedirs(newpath)
# make new folder
platform_path = 'platform_package/' + target
if not path.exists(platform_path):
makedirs(platform_path)
# copy binary it over
my_binary_name = path.basename(binary_path)
new_binary_path = platform_path + "/" + my_binary_name
copy(binary_path, new_binary_path)
chmod(new_binary_path, 0o555) # everyone can read & execute, nobody can write
# make a package.json for it
write_package_json(platform_path, target, my_binary_name)

View File

@@ -0,0 +1,21 @@
def convert_cpu_arch_to_npm_cpu_arch(arch):
if arch == "x86_64":
return "x64"
if arch == "i686":
return "i32"
if arch == "aarch64":
return "arm64"
if arch == "armv7" or arch == "arm":
return "arm"
print("architecture might not be known by nodejs, please make sure it can be returned by 'process.arch':", arch)
return arch
def convert_os_to_npm_os(os):
if os == "windows":
return "win32"
if os == "darwin" or os == "linux":
return os
if os.startswith("android"):
return "android"
print("architecture might not be known by nodejs, please make sure it can be returned by 'process.platform':", os)
return os

View File

@@ -0,0 +1,27 @@
import tomllib
import json
from .convert_platform import convert_cpu_arch_to_npm_cpu_arch, convert_os_to_npm_os
def write_package_json(platform_path, rust_target, my_binary_name):
if len(rust_target.split("-")) == 3:
[cpu_arch, vendor, os] = rust_target.split("-")
else:
[cpu_arch, vendor, os, _env] = rust_target.split("-")
# read version
tomlfile = open("../../Cargo.toml", 'rb')
version = tomllib.load(tomlfile)['package']['version']
package_json = dict({
"name": "@deltachat/stdio-rpc-server-" + convert_os_to_npm_os(os) + "-" + convert_cpu_arch_to_npm_cpu_arch(cpu_arch),
"version": version,
"os": [convert_os_to_npm_os(os)],
"cpu": [convert_cpu_arch_to_npm_cpu_arch(cpu_arch)],
"main": my_binary_name,
"license": "MPL-2.0"
})
file = open(platform_path + "/package.json", 'w')
file.write(json.dumps(package_json, indent=4))

View File

@@ -0,0 +1,57 @@
import fs from "node:fs/promises";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const expected_cwd = join(dirname(fileURLToPath(import.meta.url)), "..");
if (process.cwd() !== expected_cwd) {
console.error(
"CWD missmatch: this script needs to be run from " + expected_cwd,
{ actual: process.cwd(), expected: expected_cwd }
);
process.exit(1);
}
// whether to use local paths instead of npm registry version number for the prebuilds in optionalDependencies
// useful for local development
const is_local = process.argv.includes("--local");
const package_json = JSON.parse(await fs.readFile("./package.json", "utf8"));
const cargo_toml = await fs.readFile("../Cargo.toml", "utf8");
const version = cargo_toml
.split("\n")
.find((line) => line.includes("version"))
.split('"')[1];
const platform_packages_dir = "./platform_package";
const platform_package_names = await Promise.all(
(await fs.readdir(platform_packages_dir)).map(async (name) => {
const p = JSON.parse(
await fs.readFile(
join(platform_packages_dir, name, "package.json"),
"utf8"
)
);
if (p.version !== version) {
console.error(
name,
"has a different version than the version of the rpc server.",
{ rpc_server: version, platform_package: p.version }
);
throw new Error("version missmatch");
}
return { folder_name: name, package_name: p.name };
})
);
package_json.version = version;
package_json.optionalDependencies = {};
for (const { folder_name, package_name } of platform_package_names) {
package_json.optionalDependencies[package_name] = is_local
? `file:${expected_cwd}/platform_package/${folder_name}` // npm seems to work better with an absolute path here
: version;
}
await fs.writeFile("./package.json", JSON.stringify(package_json, null, 4));