Merge branch 'wasix-core-changes' into wasix

This commit is contained in:
Christoph Herzog
2022-12-27 15:08:28 +01:00
74 changed files with 4856 additions and 978 deletions

View File

@ -213,9 +213,6 @@ jobs:
# shell: bash # shell: bash
# run: | # run: |
# make build-wasmer-wasm # make build-wasmer-wasm
- name: Build Wapm binary
run: |
make build-wapm
- name: Install Nightly Rust for Headless - name: Install Nightly Rust for Headless
uses: dtolnay/rust-toolchain@nightly uses: dtolnay/rust-toolchain@nightly
with: with:

View File

@ -35,7 +35,7 @@ jobs:
make build-wasmer-wasm && make build-wasmer-wasm &&
mkdir target/wasm32-wasi/release/cloudcompiler mkdir target/wasm32-wasi/release/cloudcompiler
cp target/wasm32-wasi/release/wasmer-compiler.wasm target/wasm32-wasi/release/cloudcompiler/cloudcompiler.wasm && cp target/wasm32-wasi/release/wasmer-compiler.wasm target/wasm32-wasi/release/cloudcompiler/cloudcompiler.wasm &&
cat << EOF > target/wasm32-wasi/release/cloudcompiler/wapm.toml cat << EOF > target/wasm32-wasi/release/cloudcompiler/wasmer.toml
[package] [package]
name = "${{ secrets.WAPM_DEV_USERNAME }}/cloudcompiler" name = "${{ secrets.WAPM_DEV_USERNAME }}/cloudcompiler"
version = "0.1.0" version = "0.1.0"
@ -55,17 +55,17 @@ jobs:
name = "cloudcompiler" name = "cloudcompiler"
module = "cloudcompiler" module = "cloudcompiler"
EOF EOF
- name: Fix wapm.toml version - name: Fix wasmer.toml version
run: | run: |
echo $(git tag | tail -n1) > ./version.txt echo $(git tag | tail -n1) > ./version.txt
v=$(git describe --tags --abbrev=0) && \ v=$(git describe --tags --abbrev=0) && \
echo "version = ${v}" && echo "version = ${v}" &&
sed -i "s/version = \".*\"/version = \"${v}\"/g" target/wasm32-wasi/release/cloudcompiler/wapm.toml \ sed -i "s/version = \".*\"/version = \"${v}\"/g" target/wasm32-wasi/release/cloudcompiler/wasmer.toml \
- name: Build cloudcompiler.wasm - name: Build cloudcompiler.wasm
shell: bash shell: bash
run: | run: |
git tag && git tag &&
cat target/wasm32-wasi/release/cloudcompiler/wapm.toml && cat target/wasm32-wasi/release/cloudcompiler/wasmer.toml &&
echo "ls" && echo "ls" &&
ls target/wasm32-wasi/release/cloudcompiler ls target/wasm32-wasi/release/cloudcompiler
- name: Publish to WAPM - name: Publish to WAPM

2
.gitignore vendored
View File

@ -13,6 +13,8 @@ api-docs-repo/
/src/windows-installer/WasmerInstaller.exe /src/windows-installer/WasmerInstaller.exe
/lib/c-api/wasmer.h /lib/c-api/wasmer.h
.xwin-cache .xwin-cache
wapm.toml
wasmer.toml
# Generated by tests on Android # Generated by tests on Android
/avd /avd
/core /core

View File

@ -9,6 +9,10 @@ Looking for changes that affect our C API? See the [C API Changelog](lib/c-api/C
## **Unreleased** ## **Unreleased**
## Fixed
- [#3439](https://github.com/wasmerio/wasmer/pull/3439) Use GNU/Linux frame registration code for FreeBSD too
## 3.1.0 - 12/12/2022 ## 3.1.0 - 12/12/2022
## Added ## Added

583
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -49,6 +49,7 @@ members = [
"lib/wasi-experimental-io-devices", "lib/wasi-experimental-io-devices",
"lib/wasi-local-networking", "lib/wasi-local-networking",
"lib/wasix/wasix-http-client", "lib/wasix/wasix-http-client",
"lib/wasm-interface",
"lib/c-api/tests/wasmer-c-api-test-runner", "lib/c-api/tests/wasmer-c-api-test-runner",
"lib/c-api/examples/wasmer-capi-examples-runner", "lib/c-api/examples/wasmer-capi-examples-runner",
"lib/types", "lib/types",

View File

@ -401,18 +401,6 @@ else
strip --strip-unneeded target/$(HOST_TARGET)/release/wasmer-headless strip --strip-unneeded target/$(HOST_TARGET)/release/wasmer-headless
endif endif
WAPM_VERSION = v0.5.3
get-wapm:
[ -d "wapm-cli" ] || git clone --branch $(WAPM_VERSION) https://github.com/wasmerio/wapm-cli.git
build-wapm: get-wapm
ifeq ($(IS_DARWIN), 1)
# We build it without bundling sqlite, as is included by default in macos
$(CARGO_BINARY) build $(CARGO_TARGET) --release --manifest-path wapm-cli/Cargo.toml --no-default-features --features "full packagesigning telemetry update-notifications"
else
$(CARGO_BINARY) build $(CARGO_TARGET) --release --manifest-path wapm-cli/Cargo.toml --features "telemetry update-notifications"
endif
build-docs: build-docs:
$(CARGO_BINARY) doc $(CARGO_TARGET) --release $(compiler_features) --document-private-items --no-deps --workspace --exclude wasmer-c-api $(CARGO_BINARY) doc $(CARGO_TARGET) --release $(compiler_features) --document-private-items --no-deps --workspace --exclude wasmer-c-api
@ -552,6 +540,7 @@ test-examples:
$(CARGO_BINARY) test $(CARGO_TARGET) --release $(compiler_features) --features wasi --examples $(CARGO_BINARY) test $(CARGO_TARGET) --release $(compiler_features) --features wasi --examples
test-integration-cli: test-integration-cli:
rustup target add wasm32-wasi
$(CARGO_BINARY) test $(CARGO_TARGET) --features webc_runner --no-fail-fast -p wasmer-integration-tests-cli -- --nocapture --test-threads=1 $(CARGO_BINARY) test $(CARGO_TARGET) --features webc_runner --no-fail-fast -p wasmer-integration-tests-cli -- --nocapture --test-threads=1
test-integration-ios: test-integration-ios:
@ -567,23 +556,6 @@ generate-wasi-tests:
# #
##### #####
package-wapm:
mkdir -p "package/bin"
ifneq (, $(filter 1, $(IS_DARWIN) $(IS_LINUX) $(IS_FREEBSD)))
if [ -d "wapm-cli" ]; then \
cp wapm-cli/$(TARGET_DIR)/wapm package/bin/ ;\
echo -e "#!/bin/bash\nwapm execute \"\$$@\"" > package/bin/wax ;\
chmod +x package/bin/wax ;\
fi
else
if [ -d "wapm-cli" ]; then \
cp wapm-cli/$(TARGET_DIR)/wapm package/bin/ ;\
fi
ifeq ($(IS_DARWIN), 1)
codesign -s - package/bin/wapm || true
endif
endif
package-minimal-headless-wasmer: package-minimal-headless-wasmer:
ifeq ($(IS_WINDOWS), 1) ifeq ($(IS_WINDOWS), 1)
if [ -f "target/$(HOST_TARGET)/release/wasmer-headless.exe" ]; then \ if [ -f "target/$(HOST_TARGET)/release/wasmer-headless.exe" ]; then \

View File

@ -49,7 +49,13 @@ notice = "warn"
# output a note when they are encountered. # output a note when they are encountered.
ignore = [ ignore = [
#"RUSTSEC-0000-0000", #"RUSTSEC-0000-0000",
] ]
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
# See Git Authentication for more information about setting up git authentication.
git-fetch-with-cli = true
# Threshold for security vulnerabilities, any vulnerability with a CVSS score # Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories # lower than the range specified will be ignored. Note that ignored advisories
# will still output a note when they are encountered. # will still output a note when they are encountered.
@ -105,13 +111,21 @@ confidence-threshold = 0.8
exceptions = [ exceptions = [
# Each entry is the crate and version constraint, and its specific allow # Each entry is the crate and version constraint, and its specific allow
# list # list
{ allow = ["LicenseRef-LICENSE.txt"], name = "webc", version = "*" },
] ]
# Some crates don't have (easily) machine readable licensing information, # Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the # adding a clarification entry for it allows you to manually specify the
# licensing information # licensing information
#[[licenses.clarify]] [[licenses.clarify]]
name = "webc"
version = "*"
expression = "BSL-1.0"
license-files = [
{ path = "LICENSE.txt", hash = 0xa2180a97 }
]
# The name of the crate the clarification applies to # The name of the crate the clarification applies to
#name = "ring" #name = "ring"
# The optional version constraint for the crate # The optional version constraint for the crate

View File

@ -93,7 +93,6 @@ impl Memory {
pub(crate) fn new_internal(ty: MemoryType) -> Result<js_sys::WebAssembly::Memory, MemoryError> { pub(crate) fn new_internal(ty: MemoryType) -> Result<js_sys::WebAssembly::Memory, MemoryError> {
let descriptor = js_sys::Object::new(); let descriptor = js_sys::Object::new();
#[allow(unused_unsafe)] #[allow(unused_unsafe)]
#[allow(unused_unsafe)]
unsafe { unsafe {
js_sys::Reflect::set(&descriptor, &"initial".into(), &ty.minimum.0.into()).unwrap(); js_sys::Reflect::set(&descriptor, &"initial".into(), &ty.minimum.0.into()).unwrap();
if let Some(max) = ty.maximum { if let Some(max) = ty.maximum {

View File

@ -629,15 +629,6 @@ impl Module {
ExportsIterator::new(iter, exports.length() as usize) ExportsIterator::new(iter, exports.length() as usize)
} }
/// Returns true if the module is still ok - this will be
/// false if the module was passed between threads in a
/// way that it became undefined (JS does not share objects
/// between threads except via a post_message())
pub fn is_ok(&self) -> bool {
let val = JsValue::from(&self.module);
!val.is_undefined() && !val.is_null()
}
/// Get the custom sections of the module given a `name`. /// Get the custom sections of the module given a `name`.
/// ///
/// # Important /// # Important

View File

@ -1,34 +1,31 @@
use wasmer_types::RawValue;
use wasmer_vm::{
on_host_stack, raise_user_trap, resume_panic, InternalStoreHandle, StoreHandle, VMContext,
VMDynamicFunctionContext, VMExtern, VMFuncRef, VMFunction, VMFunctionBody, VMFunctionKind,
VMTrampoline,
};
use crate::sys::exports::{ExportError, Exportable}; use crate::sys::exports::{ExportError, Exportable};
use crate::sys::externals::Extern; use crate::sys::externals::Extern;
use crate::sys::store::{AsStoreMut, AsStoreRef}; use crate::sys::store::{AsStoreMut, AsStoreRef};
use crate::sys::FunctionType; use crate::sys::{FunctionType, RuntimeError, TypedFunction};
use crate::sys::TypedFunction;
use crate::FunctionEnv; use crate::FunctionEnv;
pub use inner::{FromToNativeWasmType, HostFunction, WasmTypeList, WithEnv, WithoutEnv}; pub use inner::{FromToNativeWasmType, HostFunction, WasmTypeList, WithEnv, WithoutEnv};
#[cfg(feature = "compiler")] #[cfg(feature = "compiler")]
use { use {
crate::{ crate::{
sys::{ sys::store::{StoreInner, StoreMut},
store::{StoreInner, StoreMut},
RuntimeError,
},
FunctionEnvMut, Value, FunctionEnvMut, Value,
}, },
inner::StaticFunction, inner::StaticFunction,
std::{cell::UnsafeCell, cmp::max, ffi::c_void}, std::{cell::UnsafeCell, cmp::max, ffi::c_void},
wasmer_types::RawValue,
wasmer_vm::{ wasmer_vm::{
on_host_stack, raise_user_trap, resume_panic, wasmer_call_trampoline, MaybeInstanceOwned, wasmer_call_trampoline, MaybeInstanceOwned, VMCallerCheckedAnyfunc, VMFunctionContext,
VMCallerCheckedAnyfunc, VMContext, VMDynamicFunctionContext, VMFunctionBody,
VMFunctionContext, VMTrampoline,
}, },
}; };
use wasmer_vm::{
InternalStoreHandle, StoreHandle, VMExtern, VMFuncRef, VMFunction, VMFunctionKind,
};
/// A WebAssembly `function` instance. /// A WebAssembly `function` instance.
/// ///
/// A function instance is the runtime representation of a function. /// A function instance is the runtime representation of a function.
@ -803,19 +800,16 @@ impl<'a> Exportable<'a> for Function {
} }
/// Host state for a dynamic function. /// Host state for a dynamic function.
#[cfg(feature = "compiler")]
pub(crate) struct DynamicFunction<F> { pub(crate) struct DynamicFunction<F> {
func: F, func: F,
} }
#[cfg(feature = "compiler")]
impl<F> DynamicFunction<F> impl<F> DynamicFunction<F>
where where
F: Fn(*mut RawValue) -> Result<(), RuntimeError> + 'static, F: Fn(*mut RawValue) -> Result<(), RuntimeError> + 'static,
{ {
// This function wraps our func, to make it compatible with the // This function wraps our func, to make it compatible with the
// reverse trampoline signature // reverse trampoline signature
#[cfg(feature = "compiler")]
unsafe extern "C" fn func_wrapper( unsafe extern "C" fn func_wrapper(
this: &mut VMDynamicFunctionContext<Self>, this: &mut VMDynamicFunctionContext<Self>,
values_vec: *mut RawValue, values_vec: *mut RawValue,
@ -832,12 +826,10 @@ where
} }
} }
#[cfg(feature = "compiler")]
fn func_body_ptr(&self) -> *const VMFunctionBody { fn func_body_ptr(&self) -> *const VMFunctionBody {
Self::func_wrapper as *const VMFunctionBody Self::func_wrapper as *const VMFunctionBody
} }
#[cfg(feature = "compiler")]
fn call_trampoline_address(&self) -> VMTrampoline { fn call_trampoline_address(&self) -> VMTrampoline {
Self::call_trampoline Self::call_trampoline
} }
@ -870,7 +862,6 @@ mod inner {
use crate::sys::NativeWasmTypeInto; use crate::sys::NativeWasmTypeInto;
use crate::{AsStoreMut, AsStoreRef, ExternRef, FunctionEnv, StoreMut}; use crate::{AsStoreMut, AsStoreRef, ExternRef, FunctionEnv, StoreMut};
#[cfg(feature = "compiler")]
use crate::Function; use crate::Function;
/// A trait to convert a Rust value to a `WasmNativeType` value, /// A trait to convert a Rust value to a `WasmNativeType` value,

View File

@ -267,11 +267,11 @@ impl Module {
/// # } /// # }
/// ``` /// ```
pub unsafe fn deserialize( pub unsafe fn deserialize(
store: &impl AsStoreRef, engine: &impl AsEngineRef,
bytes: impl IntoBytes, bytes: impl IntoBytes,
) -> Result<Self, DeserializeError> { ) -> Result<Self, DeserializeError> {
let bytes = bytes.into_bytes(); let bytes = bytes.into_bytes();
let artifact = store.as_store_ref().engine().deserialize(&bytes)?; let artifact = engine.as_engine_ref().engine().deserialize(&bytes)?;
Ok(Self::from_artifact(artifact)) Ok(Self::from_artifact(artifact))
} }
@ -294,11 +294,11 @@ impl Module {
/// # } /// # }
/// ``` /// ```
pub unsafe fn deserialize_from_file( pub unsafe fn deserialize_from_file(
store: &impl AsStoreRef, engine: &impl AsEngineRef,
path: impl AsRef<Path>, path: impl AsRef<Path>,
) -> Result<Self, DeserializeError> { ) -> Result<Self, DeserializeError> {
let artifact = store let artifact = engine
.as_store_ref() .as_engine_ref()
.engine() .engine()
.deserialize_from_file(path.as_ref())?; .deserialize_from_file(path.as_ref())?;
Ok(Self::from_artifact(artifact)) Ok(Self::from_artifact(artifact))
@ -453,15 +453,6 @@ impl Module {
self.module_info.exports() self.module_info.exports()
} }
/// Returns true if the module is still ok - this will be
/// false if the module was passed between threads in a
/// way that it became undefined (JS does not share objects
/// between threads except via a post_message())
pub fn is_ok(&self) -> bool {
// As RUST is a type safe language modules in SYS are always ok
true
}
/// Get the custom sections of the module given a `name`. /// Get the custom sections of the module given a `name`.
/// ///
/// # Important /// # Important

View File

@ -210,28 +210,25 @@ fn memory_grow() -> Result<(), String> {
fn function_new() -> Result<(), String> { fn function_new() -> Result<(), String> {
let mut store = Store::default(); let mut store = Store::default();
let function = Function::new_typed(&mut store, || {}); let function = Function::new_typed(&mut store, || {});
assert_eq!( assert_eq!(function.ty(&mut store), FunctionType::new(vec![], vec![]));
function.ty(&mut store).clone(),
FunctionType::new(vec![], vec![])
);
let function = Function::new_typed(&mut store, |_a: i32| {}); let function = Function::new_typed(&mut store, |_a: i32| {});
assert_eq!( assert_eq!(
function.ty(&mut store).clone(), function.ty(&mut store),
FunctionType::new(vec![Type::I32], vec![]) FunctionType::new(vec![Type::I32], vec![])
); );
let function = Function::new_typed(&mut store, |_a: i32, _b: i64, _c: f32, _d: f64| {}); let function = Function::new_typed(&mut store, |_a: i32, _b: i64, _c: f32, _d: f64| {});
assert_eq!( assert_eq!(
function.ty(&mut store).clone(), function.ty(&mut store),
FunctionType::new(vec![Type::I32, Type::I64, Type::F32, Type::F64], vec![]) FunctionType::new(vec![Type::I32, Type::I64, Type::F32, Type::F64], vec![])
); );
let function = Function::new_typed(&mut store, || -> i32 { 1 }); let function = Function::new_typed(&mut store, || -> i32 { 1 });
assert_eq!( assert_eq!(
function.ty(&mut store).clone(), function.ty(&mut store),
FunctionType::new(vec![], vec![Type::I32]) FunctionType::new(vec![], vec![Type::I32])
); );
let function = Function::new_typed(&mut store, || -> (i32, i64, f32, f64) { (1, 2, 3.0, 4.0) }); let function = Function::new_typed(&mut store, || -> (i32, i64, f32, f64) { (1, 2, 3.0, 4.0) });
assert_eq!( assert_eq!(
function.ty(&mut store).clone(), function.ty(&mut store),
FunctionType::new(vec![], vec![Type::I32, Type::I64, Type::F32, Type::F64]) FunctionType::new(vec![], vec![Type::I32, Type::I64, Type::F32, Type::F64])
); );
Ok(()) Ok(())
@ -246,14 +243,11 @@ fn function_new_env() -> Result<(), String> {
let my_env = MyEnv {}; let my_env = MyEnv {};
let env = FunctionEnv::new(&mut store, my_env); let env = FunctionEnv::new(&mut store, my_env);
let function = Function::new_typed_with_env(&mut store, &env, |_env: FunctionEnvMut<MyEnv>| {}); let function = Function::new_typed_with_env(&mut store, &env, |_env: FunctionEnvMut<MyEnv>| {});
assert_eq!( assert_eq!(function.ty(&mut store), FunctionType::new(vec![], vec![]));
function.ty(&mut store).clone(),
FunctionType::new(vec![], vec![])
);
let function = let function =
Function::new_typed_with_env(&mut store, &env, |_env: FunctionEnvMut<MyEnv>, _a: i32| {}); Function::new_typed_with_env(&mut store, &env, |_env: FunctionEnvMut<MyEnv>, _a: i32| {});
assert_eq!( assert_eq!(
function.ty(&mut store).clone(), function.ty(&mut store),
FunctionType::new(vec![Type::I32], vec![]) FunctionType::new(vec![Type::I32], vec![])
); );
let function = Function::new_typed_with_env( let function = Function::new_typed_with_env(
@ -262,13 +256,13 @@ fn function_new_env() -> Result<(), String> {
|_env: FunctionEnvMut<MyEnv>, _a: i32, _b: i64, _c: f32, _d: f64| {}, |_env: FunctionEnvMut<MyEnv>, _a: i32, _b: i64, _c: f32, _d: f64| {},
); );
assert_eq!( assert_eq!(
function.ty(&mut store).clone(), function.ty(&mut store),
FunctionType::new(vec![Type::I32, Type::I64, Type::F32, Type::F64], vec![]) FunctionType::new(vec![Type::I32, Type::I64, Type::F32, Type::F64], vec![])
); );
let function = let function =
Function::new_typed_with_env(&mut store, &env, |_env: FunctionEnvMut<MyEnv>| -> i32 { 1 }); Function::new_typed_with_env(&mut store, &env, |_env: FunctionEnvMut<MyEnv>| -> i32 { 1 });
assert_eq!( assert_eq!(
function.ty(&mut store).clone(), function.ty(&mut store),
FunctionType::new(vec![], vec![Type::I32]) FunctionType::new(vec![], vec![Type::I32])
); );
let function = Function::new_typed_with_env( let function = Function::new_typed_with_env(
@ -277,7 +271,7 @@ fn function_new_env() -> Result<(), String> {
|_env: FunctionEnvMut<MyEnv>| -> (i32, i64, f32, f64) { (1, 2, 3.0, 4.0) }, |_env: FunctionEnvMut<MyEnv>| -> (i32, i64, f32, f64) { (1, 2, 3.0, 4.0) },
); );
assert_eq!( assert_eq!(
function.ty(&mut store).clone(), function.ty(&mut store),
FunctionType::new(vec![], vec![Type::I32, Type::I64, Type::F32, Type::F64]) FunctionType::new(vec![], vec![Type::I32, Type::I64, Type::F32, Type::F64])
); );
Ok(()) Ok(())
@ -294,35 +288,35 @@ fn function_new_dynamic() -> Result<(), String> {
&function_type, &function_type,
|_values: &[Value]| unimplemented!(), |_values: &[Value]| unimplemented!(),
); );
assert_eq!(function.ty(&mut store).clone(), function_type); assert_eq!(function.ty(&mut store), function_type);
let function_type = FunctionType::new(vec![Type::I32], vec![]); let function_type = FunctionType::new(vec![Type::I32], vec![]);
let function = Function::new( let function = Function::new(
&mut store, &mut store,
&function_type, &function_type,
|_values: &[Value]| unimplemented!(), |_values: &[Value]| unimplemented!(),
); );
assert_eq!(function.ty(&mut store).clone(), function_type); assert_eq!(function.ty(&mut store), function_type);
let function_type = FunctionType::new(vec![Type::I32, Type::I64, Type::F32, Type::F64], vec![]); let function_type = FunctionType::new(vec![Type::I32, Type::I64, Type::F32, Type::F64], vec![]);
let function = Function::new( let function = Function::new(
&mut store, &mut store,
&function_type, &function_type,
|_values: &[Value]| unimplemented!(), |_values: &[Value]| unimplemented!(),
); );
assert_eq!(function.ty(&mut store).clone(), function_type); assert_eq!(function.ty(&mut store), function_type);
let function_type = FunctionType::new(vec![], vec![Type::I32]); let function_type = FunctionType::new(vec![], vec![Type::I32]);
let function = Function::new( let function = Function::new(
&mut store, &mut store,
&function_type, &function_type,
|_values: &[Value]| unimplemented!(), |_values: &[Value]| unimplemented!(),
); );
assert_eq!(function.ty(&mut store).clone(), function_type); assert_eq!(function.ty(&mut store), function_type);
let function_type = FunctionType::new(vec![], vec![Type::I32, Type::I64, Type::F32, Type::F64]); let function_type = FunctionType::new(vec![], vec![Type::I32, Type::I64, Type::F32, Type::F64]);
let function = Function::new( let function = Function::new(
&mut store, &mut store,
&function_type, &function_type,
|_values: &[Value]| unimplemented!(), |_values: &[Value]| unimplemented!(),
); );
assert_eq!(function.ty(&mut store).clone(), function_type); assert_eq!(function.ty(&mut store), function_type);
// Using array signature // Using array signature
let function_type = ([Type::V128], [Type::I32, Type::F32, Type::F64]); let function_type = ([Type::V128], [Type::I32, Type::F32, Type::F64]);
@ -356,7 +350,7 @@ fn function_new_dynamic_env() -> Result<(), String> {
&function_type, &function_type,
|_env: FunctionEnvMut<MyEnv>, _values: &[Value]| unimplemented!(), |_env: FunctionEnvMut<MyEnv>, _values: &[Value]| unimplemented!(),
); );
assert_eq!(function.ty(&mut store).clone(), function_type); assert_eq!(function.ty(&mut store), function_type);
let function_type = FunctionType::new(vec![Type::I32], vec![]); let function_type = FunctionType::new(vec![Type::I32], vec![]);
let function = Function::new_with_env( let function = Function::new_with_env(
&mut store, &mut store,
@ -364,7 +358,7 @@ fn function_new_dynamic_env() -> Result<(), String> {
&function_type, &function_type,
|_env: FunctionEnvMut<MyEnv>, _values: &[Value]| unimplemented!(), |_env: FunctionEnvMut<MyEnv>, _values: &[Value]| unimplemented!(),
); );
assert_eq!(function.ty(&mut store).clone(), function_type); assert_eq!(function.ty(&mut store), function_type);
let function_type = FunctionType::new(vec![Type::I32, Type::I64, Type::F32, Type::F64], vec![]); let function_type = FunctionType::new(vec![Type::I32, Type::I64, Type::F32, Type::F64], vec![]);
let function = Function::new_with_env( let function = Function::new_with_env(
&mut store, &mut store,
@ -372,7 +366,7 @@ fn function_new_dynamic_env() -> Result<(), String> {
&function_type, &function_type,
|_env: FunctionEnvMut<MyEnv>, _values: &[Value]| unimplemented!(), |_env: FunctionEnvMut<MyEnv>, _values: &[Value]| unimplemented!(),
); );
assert_eq!(function.ty(&mut store).clone(), function_type); assert_eq!(function.ty(&mut store), function_type);
let function_type = FunctionType::new(vec![], vec![Type::I32]); let function_type = FunctionType::new(vec![], vec![Type::I32]);
let function = Function::new_with_env( let function = Function::new_with_env(
&mut store, &mut store,
@ -380,7 +374,7 @@ fn function_new_dynamic_env() -> Result<(), String> {
&function_type, &function_type,
|_env: FunctionEnvMut<MyEnv>, _values: &[Value]| unimplemented!(), |_env: FunctionEnvMut<MyEnv>, _values: &[Value]| unimplemented!(),
); );
assert_eq!(function.ty(&mut store).clone(), function_type); assert_eq!(function.ty(&mut store), function_type);
let function_type = FunctionType::new(vec![], vec![Type::I32, Type::I64, Type::F32, Type::F64]); let function_type = FunctionType::new(vec![], vec![Type::I32, Type::I64, Type::F32, Type::F64]);
let function = Function::new_with_env( let function = Function::new_with_env(
&mut store, &mut store,
@ -388,7 +382,7 @@ fn function_new_dynamic_env() -> Result<(), String> {
&function_type, &function_type,
|_env: FunctionEnvMut<MyEnv>, _values: &[Value]| unimplemented!(), |_env: FunctionEnvMut<MyEnv>, _values: &[Value]| unimplemented!(),
); );
assert_eq!(function.ty(&mut store).clone(), function_type); assert_eq!(function.ty(&mut store), function_type);
// Using array signature // Using array signature
let function_type = ([Type::V128], [Type::I32, Type::F32, Type::F64]); let function_type = ([Type::V128], [Type::I32, Type::F32, Type::F64]);

View File

@ -364,7 +364,7 @@ pub mod reference_types {
let global: &Global = instance.exports.get_global("global")?; let global: &Global = instance.exports.get_global("global")?;
{ {
let er = ExternRef::new(&mut store, 3usize); let er = ExternRef::new(&mut store, 3usize);
global.set(&mut store, Value::ExternRef(Some(er.clone())))?; global.set(&mut store, Value::ExternRef(Some(er)))?;
} }
let get_from_global: TypedFunction<(), Option<ExternRef>> = instance let get_from_global: TypedFunction<(), Option<ExternRef>> = instance
.exports .exports
@ -395,7 +395,7 @@ pub mod reference_types {
let er = ExternRef::new(&mut store, 3usize); let er = ExternRef::new(&mut store, 3usize);
let result = pass_extern_ref.call(&mut store, Some(er.clone())); let result = pass_extern_ref.call(&mut store, Some(er));
assert!(result.is_err()); assert!(result.is_err());
Ok(()) Ok(())
@ -439,7 +439,7 @@ pub mod reference_types {
let result = grow_table_with_ref.call(&mut store, Some(er1.clone()), 10_000)?; let result = grow_table_with_ref.call(&mut store, Some(er1.clone()), 10_000)?;
assert_eq!(result, -1); assert_eq!(result, -1);
let result = grow_table_with_ref.call(&mut store, Some(er1.clone()), 8)?; let result = grow_table_with_ref.call(&mut store, Some(er1), 8)?;
assert_eq!(result, 2); assert_eq!(result, 2);
for i in 2..10 { for i in 2..10 {
@ -451,7 +451,7 @@ pub mod reference_types {
} }
{ {
fill_table_with_ref.call(&mut store, Some(er2.clone()), 0, 2)?; fill_table_with_ref.call(&mut store, Some(er2), 0, 2)?;
} }
{ {
@ -459,7 +459,7 @@ pub mod reference_types {
table2.set(&mut store, 1, Value::ExternRef(Some(er3.clone())))?; table2.set(&mut store, 1, Value::ExternRef(Some(er3.clone())))?;
table2.set(&mut store, 2, Value::ExternRef(Some(er3.clone())))?; table2.set(&mut store, 2, Value::ExternRef(Some(er3.clone())))?;
table2.set(&mut store, 3, Value::ExternRef(Some(er3.clone())))?; table2.set(&mut store, 3, Value::ExternRef(Some(er3.clone())))?;
table2.set(&mut store, 4, Value::ExternRef(Some(er3.clone())))?; table2.set(&mut store, 4, Value::ExternRef(Some(er3)))?;
} }
{ {

View File

@ -32,7 +32,7 @@ wasmer-middlewares = { version = "=3.1.0", path = "../middlewares", optional = t
wasmer-wasi = { version = "=3.1.0", path = "../wasi", default-features = false, features = ["host-fs", "sys"], optional = true } wasmer-wasi = { version = "=3.1.0", path = "../wasi", default-features = false, features = ["host-fs", "sys"], optional = true }
wasmer-types = { version = "=3.1.0", path = "../types" } wasmer-types = { version = "=3.1.0", path = "../types" }
wasmer-vfs = { version = "=3.1.0", path = "../vfs", optional = true, default-features = false, features = ["static-fs"] } wasmer-vfs = { version = "=3.1.0", path = "../vfs", optional = true, default-features = false, features = ["static-fs"] }
webc = { version = "0.4.1", optional = true } webc = { version = "4.0.0", optional = true }
enumset = "1.0.2" enumset = "1.0.2"
cfg-if = "1.0" cfg-if = "1.0"
lazy_static = "1.4" lazy_static = "1.4"

View File

@ -4,7 +4,7 @@
use crate::hash::Hash; use crate::hash::Hash;
use std::error::Error; use std::error::Error;
use wasmer::{Module, Store}; use wasmer::{AsEngineRef, Module};
/// A generic cache for storing and loading compiled wasm modules. /// A generic cache for storing and loading compiled wasm modules.
pub trait Cache { pub trait Cache {
@ -17,7 +17,11 @@ pub trait Cache {
/// ///
/// # Safety /// # Safety
/// This function is unsafe as the cache store could be tampered with. /// This function is unsafe as the cache store could be tampered with.
unsafe fn load(&self, store: &Store, key: Hash) -> Result<Module, Self::DeserializeError>; unsafe fn load(
&self,
engine: &impl AsEngineRef,
key: Hash,
) -> Result<Module, Self::DeserializeError>;
/// Store a [`Module`] into the cache with the given [`Hash`]. /// Store a [`Module`] into the cache with the given [`Hash`].
fn store(&mut self, key: Hash, module: &Module) -> Result<(), Self::SerializeError>; fn store(&mut self, key: Hash, module: &Module) -> Result<(), Self::SerializeError>;

View File

@ -4,7 +4,7 @@ use crate::hash::Hash;
use std::fs::{create_dir_all, File}; use std::fs::{create_dir_all, File};
use std::io::{self, Write}; use std::io::{self, Write};
use std::path::PathBuf; use std::path::PathBuf;
use wasmer::{DeserializeError, Module, SerializeError, Store}; use wasmer::{AsEngineRef, DeserializeError, Module, SerializeError};
/// Representation of a directory that contains compiled wasm artifacts. /// Representation of a directory that contains compiled wasm artifacts.
/// ///
@ -91,14 +91,18 @@ impl Cache for FileSystemCache {
type DeserializeError = DeserializeError; type DeserializeError = DeserializeError;
type SerializeError = SerializeError; type SerializeError = SerializeError;
unsafe fn load(&self, store: &Store, key: Hash) -> Result<Module, Self::DeserializeError> { unsafe fn load(
&self,
engine: &impl AsEngineRef,
key: Hash,
) -> Result<Module, Self::DeserializeError> {
let filename = if let Some(ref ext) = self.ext { let filename = if let Some(ref ext) = self.ext {
format!("{}.{}", key.to_string(), ext) format!("{}.{}", key.to_string(), ext)
} else { } else {
key.to_string() key.to_string()
}; };
let path = self.path.join(filename); let path = self.path.join(filename);
let ret = Module::deserialize_from_file(store, path.clone()); let ret = Module::deserialize_from_file(engine, path.clone());
if ret.is_err() { if ret.is_err() {
// If an error occurs while deserializing then we can not trust it anymore // If an error occurs while deserializing then we can not trust it anymore
// so delete the cache file // so delete the cache file

View File

@ -37,9 +37,11 @@ wasmer-wasi-experimental-io-devices = { version = "=3.1.0", path = "../wasi-expe
wasmer-wast = { version = "=3.1.0", path = "../../tests/lib/wast", optional = true } wasmer-wast = { version = "=3.1.0", path = "../../tests/lib/wast", optional = true }
wasmer-cache = { version = "=3.1.0", path = "../cache", optional = true } wasmer-cache = { version = "=3.1.0", path = "../cache", optional = true }
wasmer-types = { version = "=3.1.0", path = "../types" } wasmer-types = { version = "=3.1.0", path = "../types" }
wasmer-registry = { version = "=3.1.0", path = "../registry" } wasmer-registry = { version = "=4.0.0", path = "../registry" }
wasmer-object = { version = "=3.1.0", path = "../object", optional = true } wasmer-object = { version = "=3.1.0", path = "../object", optional = true }
wasmer-vfs = { version = "=3.1.0", path = "../vfs", default-features = false, features = ["host-fs"] } wasmer-vfs = { version = "=3.1.0", path = "../vfs", default-features = false, features = ["host-fs"] }
wasmer-wasm-interface = { version = "3.1.0", path = "../wasm-interface" }
wasmparser = "0.51.4"
atty = "0.2" atty = "0.2"
colored = "2.0" colored = "2.0"
anyhow = "1.0" anyhow = "1.0"
@ -52,7 +54,6 @@ bytesize = "1.0"
cfg-if = "1.0" cfg-if = "1.0"
# For debug feature # For debug feature
fern = { version = "0.6", features = ["colored"], optional = true } fern = { version = "0.6", features = ["colored"], optional = true }
log = { version = "0.4", optional = true }
tempfile = "3" tempfile = "3"
tempdir = "0.3.7" tempdir = "0.3.7"
http_req = { version="^0.8", default-features = false, features = ["rust-tls"] } http_req = { version="^0.8", default-features = false, features = ["rust-tls"] }
@ -62,18 +63,28 @@ dirs = { version = "4.0" }
serde_json = { version = "1.0" } serde_json = { version = "1.0" }
target-lexicon = { version = "0.12", features = ["std"] } target-lexicon = { version = "0.12", features = ["std"] }
prettytable-rs = "0.9.0" prettytable-rs = "0.9.0"
wapm-toml = "0.2.0" wasmer-toml = "0.5.0"
walkdir = "2.3.2" walkdir = "2.3.2"
regex = "1.6.0" regex = "1.6.0"
toml = "0.5.9" toml = "0.5.9"
url = "2.3.1" url = "2.3.1"
libc = { version = "^0.2", default-features = false } libc = { version = "^0.2", default-features = false }
nuke-dir = { version = "0.1.0", optional = true } nuke-dir = { version = "0.1.0", optional = true }
webc = { version = "0.4.1", optional = true } webc = { version = "4.0.0", optional = true }
isatty = "0.1.9" isatty = "0.1.9"
dialoguer = "0.10.2" dialoguer = "0.10.2"
tldextract = "0.6.0" tldextract = "0.6.0"
hex = "0.4.3" hex = "0.4.3"
flate2 = "1.0.25"
cargo_metadata = "0.15.2"
rusqlite = { version = "0.28.0", features = ["bundled"] }
tar = "0.4.38"
thiserror = "1.0.37"
time = { version = "0.3.17", default-features = false, features = ["parsing", "std", "formatting"] }
log = "0.4.17"
minisign = "0.7.2"
semver = "1.0.14"
rpassword = "7.2.0"
[build-dependencies] [build-dependencies]
chrono = { version = "^0.4", default-features = false, features = [ "std", "clock" ] } chrono = { version = "^0.4", default-features = false, features = [ "std", "clock" ] }
@ -146,7 +157,7 @@ llvm = [
"wasmer-compiler-llvm", "wasmer-compiler-llvm",
"compiler", "compiler",
] ]
debug = ["fern", "log"] debug = ["fern", "wasmer-wasi/logging"]
disable-all-logging = ["wasmer-wasi/disable-all-logging"] disable-all-logging = ["wasmer-wasi/disable-all-logging"]
headless = [] headless = []
headless-minimal = ["headless", "disable-all-logging", "wasi"] headless-minimal = ["headless", "disable-all-logging", "wasi"]

View File

@ -0,0 +1,27 @@
CREATE TABLE personal_keys
(
id integer primary key,
active integer not null,
public_key_id text not null UNIQUE,
public_key_value text not null UNIQUE,
private_key_location text UNIQUE,
key_type_identifier text not null,
date_added text not null
);
CREATE TABLE wapm_users
(
id integer primary key,
name text not null UNIQUE
);
CREATE TABLE wapm_public_keys
(
id integer primary key,
public_key_id text not null UNIQUE,
user_key integer not null,
public_key_value text not null UNIQUE,
key_type_identifier text not null,
date_added text not null,
FOREIGN KEY(user_key) REFERENCES wapm_users(id)
);

View File

@ -0,0 +1,9 @@
CREATE TABLE wasm_contracts
(
id integer primary key,
contract_name text not null,
version text not null,
date_added text not null,
content text not null,
CONSTRAINT name_version_unique UNIQUE (contract_name, version)
);

View File

@ -0,0 +1,26 @@
PRAGMA foreign_keys=off;
CREATE TABLE wasm_interfaces
(
id integer primary key,
interface_name text not null,
version text not null,
date_added text not null,
content text not null,
CONSTRAINT name_version_unique UNIQUE (interface_name, version)
);
INSERT INTO wasm_interfaces
(
id,
interface_name,
version,
date_added,
content
)
SELECT id, contract_name, version, date_added, content
FROM wasm_contracts;
DROP TABLE wasm_contracts;
PRAGMA foreign_keys=on;

View File

@ -11,7 +11,7 @@ use crate::commands::CreateObj;
#[cfg(feature = "wast")] #[cfg(feature = "wast")]
use crate::commands::Wast; use crate::commands::Wast;
use crate::commands::{ use crate::commands::{
Add, Cache, Config, Inspect, List, Login, Run, SelfUpdate, Validate, Whoami, Add, Cache, Config, Init, Inspect, List, Login, Publish, Run, SelfUpdate, Validate, Whoami,
}; };
use crate::error::PrettyError; use crate::error::PrettyError;
use clap::{CommandFactory, ErrorKind, Parser}; use clap::{CommandFactory, ErrorKind, Parser};
@ -46,6 +46,10 @@ enum WasmerCLIOptions {
/// Login into a wapm.io-like registry /// Login into a wapm.io-like registry
Login(Login), Login(Login),
/// Login into a wapm.io-like registry
#[clap(name = "publish")]
Publish(Publish),
/// Wasmer cache /// Wasmer cache
#[clap(subcommand)] #[clap(subcommand)]
Cache(Cache), Cache(Cache),
@ -135,6 +139,10 @@ enum WasmerCLIOptions {
/// Inspect a WebAssembly file /// Inspect a WebAssembly file
Inspect(Inspect), Inspect(Inspect),
/// Initializes a new wasmer.toml file
#[clap(name = "init")]
Init(Init),
/// Run spec testsuite /// Run spec testsuite
#[cfg(feature = "wast")] #[cfg(feature = "wast")]
Wast(Wast), Wast(Wast),
@ -165,8 +173,10 @@ impl WasmerCLIOptions {
Self::CreateObj(create_obj) => create_obj.execute(), Self::CreateObj(create_obj) => create_obj.execute(),
Self::Config(config) => config.execute(), Self::Config(config) => config.execute(),
Self::Inspect(inspect) => inspect.execute(), Self::Inspect(inspect) => inspect.execute(),
Self::Init(init) => init.execute(),
Self::List(list) => list.execute(), Self::List(list) => list.execute(),
Self::Login(login) => login.execute(), Self::Login(login) => login.execute(),
Self::Publish(publish) => publish.execute(),
#[cfg(feature = "wast")] #[cfg(feature = "wast")]
Self::Wast(wast) => wast.execute(), Self::Wast(wast) => wast.execute(),
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@ -224,10 +234,9 @@ fn wasmer_main_inner() -> Result<(), anyhow::Error> {
WasmerCLIOptions::Run(Run::from_binfmt_args()) WasmerCLIOptions::Run(Run::from_binfmt_args())
} else { } else {
match command.unwrap_or(&"".to_string()).as_ref() { match command.unwrap_or(&"".to_string()).as_ref() {
"add" | "cache" | "compile" | "config" | "create-exe" | "help" | "inspect" | "run" "add" | "cache" | "compile" | "config" | "create-exe" | "help" | "inspect" | "init"
| "self-update" | "validate" | "wast" | "binfmt" | "list" | "login" => { | "run" | "self-update" | "validate" | "wast" | "binfmt" | "list" | "login"
WasmerCLIOptions::parse() | "publish" => WasmerCLIOptions::parse(),
}
_ => { _ => {
WasmerCLIOptions::try_parse_from(args.iter()).unwrap_or_else(|e| { WasmerCLIOptions::try_parse_from(args.iter()).unwrap_or_else(|e| {
match e.kind() { match e.kind() {

View File

@ -10,9 +10,11 @@ mod config;
mod create_exe; mod create_exe;
#[cfg(feature = "static-artifact-create")] #[cfg(feature = "static-artifact-create")]
mod create_obj; mod create_obj;
mod init;
mod inspect; mod inspect;
mod list; mod list;
mod login; mod login;
mod publish;
mod run; mod run;
mod self_update; mod self_update;
mod validate; mod validate;
@ -31,8 +33,8 @@ pub use create_obj::*;
#[cfg(feature = "wast")] #[cfg(feature = "wast")]
pub use wast::*; pub use wast::*;
pub use { pub use {
add::*, cache::*, config::*, inspect::*, list::*, login::*, run::*, self_update::*, add::*, cache::*, config::*, init::*, inspect::*, list::*, login::*, publish::*, run::*,
validate::*, whoami::*, self_update::*, validate::*, whoami::*,
}; };
/// The kind of object format to emit. /// The kind of object format to emit.

View File

@ -2,7 +2,7 @@ use std::process::{Command, Stdio};
use anyhow::{Context, Error}; use anyhow::{Context, Error};
use clap::Parser; use clap::Parser;
use wasmer_registry::{Bindings, PartialWapmConfig, ProgrammingLanguage}; use wasmer_registry::{Bindings, ProgrammingLanguage, WasmerConfig};
/// Add a WAPM package's bindings to your application. /// Add a WAPM package's bindings to your application.
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@ -76,7 +76,9 @@ impl Add {
match &self.registry { match &self.registry {
Some(r) => Ok(r.clone()), Some(r) => Ok(r.clone()),
None => { None => {
let cfg = PartialWapmConfig::from_file() let wasmer_dir =
WasmerConfig::get_wasmer_dir().map_err(|e| anyhow::anyhow!("{e}"))?;
let cfg = WasmerConfig::from_file(&wasmer_dir)
.map_err(Error::msg) .map_err(Error::msg)
.context("Unable to load WAPM's config file")?; .context("Unable to load WAPM's config file")?;
Ok(cfg.registry.get_current_registry()) Ok(cfg.registry.get_current_registry())

View File

@ -3,10 +3,22 @@ use anyhow::{Context, Result};
use clap::Parser; use clap::Parser;
use std::env; use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::ParseBoolError;
use wasmer_registry::WasmerConfig;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
/// The options for the `wasmer config` subcommand /// The options for the `wasmer config` subcommand: `wasmer config get --OPTION` or `wasmer config set [FLAG]`
pub struct Config { pub struct Config {
#[clap(flatten)]
flags: Flags,
/// Subcommand for `wasmer config get | set`
#[clap(subcommand)]
set: Option<GetOrSet>,
}
/// Normal configuration
#[derive(Debug, Parser)]
pub struct Flags {
/// Print the installation prefix. /// Print the installation prefix.
#[clap(long, conflicts_with = "pkg-config")] #[clap(long, conflicts_with = "pkg-config")]
prefix: bool, prefix: bool,
@ -31,12 +43,118 @@ pub struct Config {
#[clap(long, conflicts_with = "pkg-config")] #[clap(long, conflicts_with = "pkg-config")]
cflags: bool, cflags: bool,
/// It outputs the necessary details for compiling /// Print the path to the wasmer configuration file where all settings are stored
#[clap(long, conflicts_with = "pkg-config")]
config_path: bool,
/// Outputs the necessary details for compiling
/// and linking a program to Wasmer, using the `pkg-config` format. /// and linking a program to Wasmer, using the `pkg-config` format.
#[clap(long)] #[clap(long)]
pkg_config: bool, pkg_config: bool,
} }
/// Subcommand for `wasmer config set`
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Parser)]
pub enum GetOrSet {
/// `wasmer config get $KEY`
#[clap(subcommand)]
Get(RetrievableConfigField),
/// `wasmer config set $KEY $VALUE`
#[clap(subcommand)]
Set(StorableConfigField),
}
/// Subcommand for `wasmer config get`
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Parser)]
pub enum RetrievableConfigField {
/// Print the registry URL of the currently active registry
#[clap(name = "registry.url")]
RegistryUrl,
/// Print the token for the currently active registry or nothing if not logged in
#[clap(name = "registry.token")]
RegistryToken,
/// Print whether telemetry is currently enabled
#[clap(name = "telemetry.enabled")]
TelemetryEnabled,
/// Print whether update notifications are enabled
#[clap(name = "update-notifications.enabled")]
UpdateNotificationsEnabled,
/// Print the proxy URL
#[clap(name = "proxy.url")]
ProxyUrl,
}
/// Setting that can be stored in the wasmer config
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Parser)]
pub enum StorableConfigField {
/// Set the registry URL of the currently active registry
#[clap(name = "registry.url")]
RegistryUrl(SetRegistryUrl),
/// Set the token for the currently active registry or nothing if not logged in
#[clap(name = "registry.token")]
RegistryToken(SetRegistryToken),
/// Set whether telemetry is currently enabled
#[clap(name = "telemetry.enabled")]
TelemetryEnabled(SetTelemetryEnabled),
/// Set whether update notifications are enabled
#[clap(name = "update-notifications.enabled")]
UpdateNotificationsEnabled(SetUpdateNotificationsEnabled),
/// Set the active proxy URL
#[clap(name = "proxy.url")]
ProxyUrl(SetProxyUrl),
}
/// Set the current active registry URL
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Parser)]
pub struct SetRegistryUrl {
/// Url of the registry
#[clap(name = "URL")]
pub url: String,
}
/// Set or change the token for the current active registry
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Parser)]
pub struct SetRegistryToken {
/// Token to set
#[clap(name = "TOKEN")]
pub token: String,
}
/// Set if update notifications are enabled
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Parser)]
pub struct SetUpdateNotificationsEnabled {
/// Whether to enable update notifications
#[clap(name = "ENABLED", possible_values = ["true", "false"])]
pub enabled: BoolString,
}
/// "true" or "false" for handling input in the CLI
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct BoolString(pub bool);
impl std::str::FromStr for BoolString {
type Err = ParseBoolError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(bool::from_str(s)?))
}
}
/// Set if telemetry is enabled
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Parser)]
pub struct SetTelemetryEnabled {
/// Whether to enable telemetry
#[clap(name = "ENABLED", possible_values = ["true", "false"])]
pub enabled: BoolString,
}
/// Set if a proxy URL should be used
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Parser)]
pub struct SetProxyUrl {
/// Set if a proxy URL should be used (empty = unset proxy)
#[clap(name = "URL")]
pub url: String,
}
impl Config { impl Config {
/// Runs logic for the `config` subcommand /// Runs logic for the `config` subcommand
pub fn execute(&self) -> Result<()> { pub fn execute(&self) -> Result<()> {
@ -44,6 +162,12 @@ impl Config {
.context("failed to retrieve the wasmer config".to_string()) .context("failed to retrieve the wasmer config".to_string())
} }
fn inner_execute(&self) -> Result<()> { fn inner_execute(&self) -> Result<()> {
if let Some(s) = self.set.as_ref() {
return s.execute();
}
let flags = &self.flags;
let key = "WASMER_DIR"; let key = "WASMER_DIR";
let wasmer_dir = env::var(key) let wasmer_dir = env::var(key)
.or_else(|e| { .or_else(|e| {
@ -65,7 +189,7 @@ impl Config {
let cflags = format!("-I{}", includedir); let cflags = format!("-I{}", includedir);
let libs = format!("-L{} -lwasmer", libdir); let libs = format!("-L{} -lwasmer", libdir);
if self.pkg_config { if flags.pkg_config {
println!("prefix={}", prefixdir); println!("prefix={}", prefixdir);
println!("exec_prefix={}", bindir); println!("exec_prefix={}", bindir);
println!("includedir={}", includedir); println!("includedir={}", includedir);
@ -79,24 +203,114 @@ impl Config {
return Ok(()); return Ok(());
} }
if self.prefix { if flags.prefix {
println!("{}", prefixdir); println!("{}", prefixdir);
} }
if self.bindir { if flags.bindir {
println!("{}", bindir); println!("{}", bindir);
} }
if self.includedir { if flags.includedir {
println!("{}", includedir); println!("{}", includedir);
} }
if self.libdir { if flags.libdir {
println!("{}", libdir); println!("{}", libdir);
} }
if self.libs { if flags.libs {
println!("{}", libs); println!("{}", libs);
} }
if self.cflags { if flags.cflags {
println!("{}", cflags); println!("{}", cflags);
} }
if flags.config_path {
let wasmer_dir = WasmerConfig::get_wasmer_dir()
.map_err(|e| anyhow::anyhow!("could not find wasmer dir: {e}"))?;
let path = WasmerConfig::get_file_location(&wasmer_dir);
println!("{}", path.display());
}
Ok(())
}
}
impl GetOrSet {
fn execute(&self) -> Result<()> {
let wasmer_dir = WasmerConfig::get_wasmer_dir()
.map_err(|e| anyhow::anyhow!("could not find wasmer dir: {e}"))?;
let config_file = WasmerConfig::get_file_location(&wasmer_dir);
let mut config = WasmerConfig::from_file(&wasmer_dir).map_err(|e| {
anyhow::anyhow!(
"could not find config file {e} at {}",
config_file.display()
)
})?;
match self {
GetOrSet::Get(g) => match g {
RetrievableConfigField::RegistryUrl => {
println!("{}", config.registry.get_current_registry());
}
RetrievableConfigField::RegistryToken => {
if let Some(s) = config
.registry
.get_login_token_for_registry(&config.registry.get_current_registry())
{
println!("{s}");
}
}
RetrievableConfigField::TelemetryEnabled => {
println!("{:?}", config.telemetry_enabled);
}
RetrievableConfigField::UpdateNotificationsEnabled => {
println!("{:?}", config.update_notifications_enabled);
}
RetrievableConfigField::ProxyUrl => {
if let Some(s) = config.proxy.url.as_ref() {
println!("{s}");
} else {
println!("none");
}
}
},
GetOrSet::Set(s) => {
match s {
StorableConfigField::RegistryUrl(s) => {
config.registry.set_current_registry(&s.url);
let current_registry = config.registry.get_current_registry();
if let Some(u) = wasmer_registry::utils::get_username(&current_registry)
.ok()
.and_then(|o| o)
{
println!(
"Successfully logged into registry {current_registry:?} as user {u:?}"
);
}
}
StorableConfigField::RegistryToken(t) => {
config.registry.set_login_token_for_registry(
&config.registry.get_current_registry(),
&t.token,
wasmer_registry::config::UpdateRegistry::LeaveAsIs,
);
}
StorableConfigField::TelemetryEnabled(t) => {
config.telemetry_enabled = t.enabled.0;
}
StorableConfigField::ProxyUrl(p) => {
if p.url == "none" || p.url.is_empty() {
config.proxy.url = None;
} else {
config.proxy.url = Some(p.url.clone());
}
}
StorableConfigField::UpdateNotificationsEnabled(u) => {
config.update_notifications_enabled = u.enabled.0;
}
}
config
.save(config_file)
.with_context(|| anyhow::anyhow!("could not save config file"))?;
}
}
Ok(()) Ok(())
} }
} }

View File

@ -0,0 +1,517 @@
use anyhow::Context;
use cargo_metadata::{CargoOpt, MetadataCommand};
use clap::Parser;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use wasmer_registry::WasmerConfig;
static NOTE: &str =
"# See more keys and definitions at https://docs.wasmer.io/ecosystem/wapm/manifest";
const NEWLINE: &str = if cfg!(windows) { "\r\n" } else { "\n" };
/// CLI args for the `wasmer init` command
#[derive(Debug, Parser)]
pub struct Init {
/// Initialize wasmer.toml for a library package
#[clap(long, group = "crate-type")]
pub lib: bool,
/// Initialize wasmer.toml for a binary package
#[clap(long, group = "crate-type")]
pub bin: bool,
/// Initialize an empty wasmer.toml
#[clap(long, group = "crate-type")]
pub empty: bool,
/// Force overwriting the wasmer.toml, even if it already exists
#[clap(long)]
pub overwrite: bool,
/// Don't display debug output
#[clap(long)]
pub quiet: bool,
/// Namespace to init with, default = current logged in user or _
#[clap(long)]
pub namespace: Option<String>,
/// Package name to init with, default = Cargo.toml name or current directory name
#[clap(long)]
pub package_name: Option<String>,
/// Version of the initialized package
#[clap(long)]
pub version: Option<semver::Version>,
/// If the `manifest-path` is a Cargo.toml, use that file to initialize the wasmer.toml
#[clap(long)]
pub manifest_path: Option<PathBuf>,
/// Add default dependencies for common packages
#[clap(long, value_enum)]
pub template: Option<Template>,
/// Include file paths into the target container filesystem
#[clap(long)]
pub include: Vec<String>,
/// Directory of the output file name. wasmer init will error if the target dir
/// already contains a wasmer.toml. Also sets the package name.
#[clap(name = "PACKAGE_PATH")]
pub out: Option<PathBuf>,
}
/// What template to use for the initialized wasmer.toml
#[derive(Debug, PartialEq, Eq, Copy, Clone, clap::ValueEnum)]
pub enum Template {
/// Add dependency on Python
Python,
/// Add dependency on JS
Js,
}
#[derive(Debug, PartialEq, Copy, Clone)]
enum BinOrLib {
Bin,
Lib,
Empty,
}
// minimal version of the Cargo.toml [package] section
#[derive(Debug, Clone)]
struct MiniCargoTomlPackage {
cargo_toml_path: PathBuf,
name: String,
version: semver::Version,
description: Option<String>,
homepage: Option<String>,
repository: Option<String>,
license: Option<String>,
readme: Option<PathBuf>,
license_file: Option<PathBuf>,
#[allow(dead_code)]
workspace_root: PathBuf,
#[allow(dead_code)]
build_dir: PathBuf,
}
static WASMER_TOML_NAME: &str = "wasmer.toml";
impl Init {
/// `wasmer init` execution
pub fn execute(&self) -> Result<(), anyhow::Error> {
let bin_or_lib = self.get_bin_or_lib()?;
// See if the directory has a Cargo.toml file, if yes, copy the license / readme, etc.
let manifest_path = match self.manifest_path.as_ref() {
Some(s) => s.clone(),
None => {
let cargo_toml_path = self
.out
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap())
.join("Cargo.toml");
cargo_toml_path
.canonicalize()
.unwrap_or_else(|_| cargo_toml_path.clone())
}
};
let cargo_toml = if manifest_path.exists() {
parse_cargo_toml(&manifest_path).ok()
} else {
None
};
let (fallback_package_name, target_file) = self.target_file()?;
if target_file.exists() && !self.overwrite {
anyhow::bail!(
"wasmer project already initialized in {}",
target_file.display(),
);
}
let constructed_manifest = construct_manifest(
cargo_toml.as_ref(),
&fallback_package_name,
self.package_name.as_deref(),
&target_file,
&manifest_path,
bin_or_lib,
self.namespace.clone(),
self.version.clone(),
self.template.as_ref(),
self.include.as_slice(),
self.quiet,
)?;
if let Some(parent) = target_file.parent() {
let _ = std::fs::create_dir_all(parent);
}
// generate the wasmer.toml and exit
Self::write_wasmer_toml(&target_file, &constructed_manifest)
}
/// Writes the metadata to a wasmer.toml file
fn write_wasmer_toml(
path: &PathBuf,
toml: &wasmer_toml::Manifest,
) -> Result<(), anyhow::Error> {
let toml_string = toml::to_string_pretty(&toml)?
.replace(
"[dependencies]",
&format!("{NOTE}{NEWLINE}{NEWLINE}[dependencies]"),
)
.lines()
.collect::<Vec<_>>()
.join(NEWLINE);
std::fs::write(&path, &toml_string)
.with_context(|| format!("Unable to write to \"{}\"", path.display()))?;
Ok(())
}
fn target_file(&self) -> Result<(String, PathBuf), anyhow::Error> {
match self.out.as_ref() {
None => {
let current_dir = std::env::current_dir()?;
let package_name = self
.package_name
.clone()
.or_else(|| {
current_dir
.canonicalize()
.ok()?
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string())
})
.ok_or_else(|| anyhow::anyhow!("no current dir name"))?;
Ok((package_name, current_dir.join(WASMER_TOML_NAME)))
}
Some(s) => {
std::fs::create_dir_all(s)
.map_err(|e| anyhow::anyhow!("{e}"))
.with_context(|| anyhow::anyhow!("{}", s.display()))?;
let package_name = self
.package_name
.clone()
.or_else(|| {
s.canonicalize()
.ok()?
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string())
})
.ok_or_else(|| anyhow::anyhow!("no dir name"))?;
Ok((package_name, s.join(WASMER_TOML_NAME)))
}
}
}
fn get_filesystem_mapping(include: &[String]) -> Option<HashMap<String, PathBuf>> {
if include.is_empty() {
return None;
}
Some(
include
.iter()
.map(|path| {
if path == "." || path == "/" {
return ("/".to_string(), Path::new("/").to_path_buf());
}
let key = format!("./{path}");
let value = PathBuf::from(format!("/{path}"));
(key, value)
})
.collect(),
)
}
fn get_command(
modules: &[wasmer_toml::Module],
bin_or_lib: BinOrLib,
) -> Option<Vec<wasmer_toml::Command>> {
match bin_or_lib {
BinOrLib::Bin => Some(
modules
.iter()
.map(|m| {
wasmer_toml::Command::V1(wasmer_toml::CommandV1 {
name: m.name.clone(),
module: m.name.clone(),
main_args: None,
package: None,
})
})
.collect(),
),
BinOrLib::Lib | BinOrLib::Empty => None,
}
}
/// Returns the dependencies based on the `--template` flag
fn get_dependencies(template: Option<&Template>) -> HashMap<String, String> {
let mut map = HashMap::default();
match template {
Some(Template::Js) => {
map.insert("quickjs".to_string(), "quickjs/quickjs@latest".to_string());
}
Some(Template::Python) => {
map.insert("python".to_string(), "python/python@latest".to_string());
}
_ => {}
}
map
}
// Returns whether the template for the wasmer.toml should be a binary, a library or an empty file
fn get_bin_or_lib(&self) -> Result<BinOrLib, anyhow::Error> {
match (self.empty, self.bin, self.lib) {
(true, false, false) => Ok(BinOrLib::Empty),
(false, true, false) => Ok(BinOrLib::Bin),
(false, false, true) => Ok(BinOrLib::Lib),
(false, false, false) => Ok(BinOrLib::Bin),
_ => anyhow::bail!("Only one of --bin, --lib, or --empty can be provided"),
}
}
/// Get bindings returns the first .wai / .wit file found and
/// optionally takes a warning callback that is triggered when > 1 .wai files are found
fn get_bindings(target_file: &Path, bin_or_lib: BinOrLib) -> Option<GetBindingsResult> {
match bin_or_lib {
BinOrLib::Bin | BinOrLib::Empty => None,
BinOrLib::Lib => target_file.parent().and_then(|parent| {
let all_bindings = walkdir::WalkDir::new(parent)
.min_depth(1)
.max_depth(3)
.follow_links(false)
.into_iter()
.filter_map(|e| e.ok())
.filter_map(|e| {
let is_wit = e.path().extension().and_then(|s| s.to_str()) == Some(".wit");
let is_wai = e.path().extension().and_then(|s| s.to_str()) == Some(".wai");
if is_wit {
Some(wasmer_toml::Bindings::Wit(wasmer_toml::WitBindings {
wit_exports: e.path().to_path_buf(),
wit_bindgen: semver::Version::parse("0.1.0").unwrap(),
}))
} else if is_wai {
Some(wasmer_toml::Bindings::Wai(wasmer_toml::WaiBindings {
exports: None,
imports: vec![e.path().to_path_buf()],
wai_version: semver::Version::parse("0.2.0").unwrap(),
}))
} else {
None
}
})
.collect::<Vec<_>>();
if all_bindings.is_empty() {
None
} else if all_bindings.len() == 1 {
Some(GetBindingsResult::OneBinding(all_bindings[0].clone()))
} else {
Some(GetBindingsResult::MultiBindings(all_bindings))
}
}),
}
}
}
enum GetBindingsResult {
OneBinding(wasmer_toml::Bindings),
MultiBindings(Vec<wasmer_toml::Bindings>),
}
impl GetBindingsResult {
fn first_binding(&self) -> Option<wasmer_toml::Bindings> {
match self {
Self::OneBinding(s) => Some(s.clone()),
Self::MultiBindings(s) => s.get(0).cloned(),
}
}
}
#[allow(clippy::too_many_arguments)]
fn construct_manifest(
cargo_toml: Option<&MiniCargoTomlPackage>,
fallback_package_name: &String,
package_name: Option<&str>,
target_file: &Path,
manifest_path: &Path,
bin_or_lib: BinOrLib,
namespace: Option<String>,
version: Option<semver::Version>,
template: Option<&Template>,
include_fs: &[String],
quiet: bool,
) -> Result<wasmer_toml::Manifest, anyhow::Error> {
if let Some(ct) = cargo_toml.as_ref() {
let msg = format!(
"NOTE: Initializing wasmer.toml file with metadata from Cargo.toml{NEWLINE} -> {}",
ct.cargo_toml_path.display()
);
if !quiet {
println!("{msg}");
}
log::warn!("{msg}");
}
let package_name = package_name.unwrap_or_else(|| {
cargo_toml
.as_ref()
.map(|p| &p.name)
.unwrap_or(fallback_package_name)
});
let wasmer_dir = WasmerConfig::get_wasmer_dir().map_err(|e| anyhow::anyhow!("{e}"))?;
let namespace = namespace.or_else(|| {
wasmer_registry::whoami(&wasmer_dir, None, None)
.ok()
.map(|o| o.1)
});
let version = version.unwrap_or_else(|| {
cargo_toml
.as_ref()
.map(|t| t.version.clone())
.unwrap_or_else(|| semver::Version::parse("0.1.0").unwrap())
});
let license = cargo_toml.as_ref().and_then(|t| t.license.clone());
let license_file = cargo_toml.as_ref().and_then(|t| t.license_file.clone());
let readme = cargo_toml.as_ref().and_then(|t| t.readme.clone());
let repository = cargo_toml.as_ref().and_then(|t| t.repository.clone());
let homepage = cargo_toml.as_ref().and_then(|t| t.homepage.clone());
let description = cargo_toml
.as_ref()
.and_then(|t| t.description.clone())
.unwrap_or_else(|| format!("Description for package {package_name}"));
let default_abi = wasmer_toml::Abi::Wasi;
let bindings = Init::get_bindings(target_file, bin_or_lib);
if let Some(GetBindingsResult::MultiBindings(m)) = bindings.as_ref() {
let found = m
.iter()
.map(|m| match m {
wasmer_toml::Bindings::Wit(wb) => {
format!("found: {}", serde_json::to_string(wb).unwrap_or_default())
}
wasmer_toml::Bindings::Wai(wb) => {
format!("found: {}", serde_json::to_string(wb).unwrap_or_default())
}
})
.collect::<Vec<_>>()
.join("\r\n");
let msg = vec![
String::new(),
" It looks like your project contains multiple *.wai files.".to_string(),
" Make sure you update the [[module.bindings]] appropriately".to_string(),
String::new(),
found,
];
let msg = msg.join("\r\n");
if !quiet {
println!("{msg}");
}
log::warn!("{msg}");
}
let modules = vec![wasmer_toml::Module {
name: package_name.to_string(),
source: cargo_toml
.as_ref()
.map(|p| {
// Normalize the path to /target/release to be relative to the parent of the Cargo.toml
let outpath = p
.build_dir
.join("release")
.join(&format!("{package_name}.wasm"));
let canonicalized_outpath = outpath.canonicalize().unwrap_or(outpath);
let outpath_str = format!("{}", canonicalized_outpath.display());
let manifest_canonicalized = manifest_path
.parent()
.and_then(|p| p.canonicalize().ok())
.unwrap_or_else(|| manifest_path.to_path_buf());
let manifest_str = format!("{}/", manifest_canonicalized.display());
let relative_str = outpath_str.replacen(&manifest_str, "", 1);
Path::new(&relative_str).to_path_buf()
})
.unwrap_or_else(|| Path::new(&format!("{package_name}.wasm")).to_path_buf()),
kind: None,
abi: default_abi,
bindings: bindings.as_ref().and_then(|b| b.first_binding()),
interfaces: Some({
let mut map = HashMap::new();
map.insert("wasi".to_string(), "0.1.0-unstable".to_string());
map
}),
}];
Ok(wasmer_toml::Manifest {
package: wasmer_toml::Package {
name: if let Some(s) = namespace {
format!("{s}/{package_name}")
} else {
package_name.to_string()
},
version,
description,
license,
license_file,
readme,
repository,
homepage,
wasmer_extra_flags: None,
disable_command_rename: false,
rename_commands_to_raw_command_name: false,
},
dependencies: Some(Init::get_dependencies(template)),
command: Init::get_command(&modules, bin_or_lib),
module: match bin_or_lib {
BinOrLib::Empty => None,
_ => Some(modules),
},
fs: Init::get_filesystem_mapping(include_fs),
base_directory_path: target_file
.parent()
.map(|o| o.to_path_buf())
.unwrap_or_else(|| target_file.to_path_buf()),
})
}
fn parse_cargo_toml(manifest_path: &PathBuf) -> Result<MiniCargoTomlPackage, anyhow::Error> {
let mut metadata = MetadataCommand::new();
metadata.manifest_path(&manifest_path);
metadata.no_deps();
metadata.features(CargoOpt::AllFeatures);
let metadata = metadata.exec().with_context(|| {
format!(
"Unable to load metadata from \"{}\"",
manifest_path.display()
)
})?;
let package = metadata
.root_package()
.ok_or_else(|| anyhow::anyhow!("no root package found in cargo metadata"))
.context(anyhow::anyhow!("{}", manifest_path.display()))?;
Ok(MiniCargoTomlPackage {
cargo_toml_path: manifest_path.clone(),
name: package.name.clone(),
version: package.version.clone(),
description: package.description.clone(),
homepage: package.homepage.clone(),
repository: package.repository.clone(),
license: package.license.clone(),
readme: package.readme.clone().map(|s| s.into_std_path_buf()),
license_file: package.license_file.clone().map(|f| f.into_std_path_buf()),
workspace_root: metadata.workspace_root.into_std_path_buf(),
build_dir: metadata
.target_directory
.into_std_path_buf()
.join("wasm32-wasi"),
})
}

View File

@ -1,4 +1,5 @@
use clap::Parser; use clap::Parser;
use wasmer_registry::WasmerConfig;
/// Subcommand for listing packages /// Subcommand for listing packages
#[derive(Debug, Copy, Clone, Parser)] #[derive(Debug, Copy, Clone, Parser)]
@ -8,8 +9,9 @@ impl List {
/// execute [List] /// execute [List]
pub fn execute(&self) -> Result<(), anyhow::Error> { pub fn execute(&self) -> Result<(), anyhow::Error> {
use prettytable::{format, row, Table}; use prettytable::{format, row, Table};
let wasmer_dir =
let rows = wasmer_registry::get_all_local_packages() WasmerConfig::get_wasmer_dir().map_err(|e| anyhow::anyhow!("no wasmer dir: {e}"))?;
let rows = wasmer_registry::get_all_local_packages(&wasmer_dir)
.into_iter() .into_iter()
.filter_map(|pkg| { .filter_map(|pkg| {
let package_root_path = pkg.path; let package_root_path = pkg.path;

View File

@ -1,6 +1,7 @@
use clap::Parser; use clap::Parser;
#[cfg(not(test))] #[cfg(not(test))]
use dialoguer::Input; use dialoguer::Input;
use wasmer_registry::WasmerConfig;
/// Subcommand for listing packages /// Subcommand for listing packages
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]
@ -51,7 +52,9 @@ impl Login {
/// execute [List] /// execute [List]
pub fn execute(&self) -> Result<(), anyhow::Error> { pub fn execute(&self) -> Result<(), anyhow::Error> {
let token = self.get_token_or_ask_user()?; let token = self.get_token_or_ask_user()?;
match wasmer_registry::login::login_and_save_token(&self.registry, &token)? { let wasmer_dir =
WasmerConfig::get_wasmer_dir().map_err(|e| anyhow::anyhow!("no wasmer dir: {e}"))?;
match wasmer_registry::login::login_and_save_token(&wasmer_dir, &self.registry, &token)? {
Some(s) => println!("Login for WAPM user {:?} saved", s), Some(s) => println!("Login for WAPM user {:?} saved", s),
None => println!( None => println!(
"Error: no user found on registry {:?} with token {:?}. Token saved regardless.", "Error: no user found on registry {:?} with token {:?}. Token saved regardless.",

View File

@ -0,0 +1,659 @@
use anyhow::Context;
use clap::Parser;
use flate2::{write::GzEncoder, Compression};
use rusqlite::{params, Connection, OpenFlags, TransactionBehavior};
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use tar::Builder;
use thiserror::Error;
use time::{self, OffsetDateTime};
use wasmer_registry::publish::SignArchiveResult;
use wasmer_registry::{WasmerConfig, PACKAGE_TOML_FALLBACK_NAME};
const MIGRATIONS: &[(i32, &str)] = &[
(0, include_str!("../../sql/migrations/0000.sql")),
(1, include_str!("../../sql/migrations/0001.sql")),
(2, include_str!("../../sql/migrations/0002.sql")),
];
const CURRENT_DATA_VERSION: usize = MIGRATIONS.len();
/// CLI options for the `wasmer publish` command
#[derive(Debug, Parser)]
pub struct Publish {
/// Registry to publish to
#[clap(long)]
pub registry: Option<String>,
/// Run the publish logic without sending anything to the registry server
#[clap(long, name = "dry-run")]
pub dry_run: bool,
/// Run the publish command without any output
#[clap(long)]
pub quiet: bool,
/// Override the package of the uploaded package in the wasmer.toml
#[clap(long)]
pub package_name: Option<String>,
/// Override the package version of the uploaded package in the wasmer.toml
#[clap(long)]
pub version: Option<semver::Version>,
/// Override the token (by default, it will use the current logged in user)
#[clap(long)]
pub token: Option<String>,
/// Skip validation of the uploaded package
#[clap(long)]
pub no_validate: bool,
/// Directory containing the `wasmer.toml` (defaults to current root dir)
#[clap(name = "PACKAGE_PATH")]
pub package_path: Option<String>,
}
#[derive(Debug, Error)]
enum PublishError {
#[error("Cannot publish without a module.")]
NoModule,
#[error("Unable to publish the \"{module}\" module because \"{}\" is not a file", path.display())]
SourceMustBeFile { module: String, path: PathBuf },
#[error("Unable to load the bindings for \"{module}\" because \"{}\" doesn't exist", path.display())]
MissingBindings { module: String, path: PathBuf },
#[error("Error building package when parsing module \"{0}\": {1}.")]
ErrorBuildingPackage(String, io::Error),
#[error(
"Path \"{0}\", specified in the manifest as part of the package file system does not exist.",
)]
MissingManifestFsPath(String),
#[error("When processing the package filesystem, found path \"{0}\" which is not a directory")]
PackageFileSystemEntryMustBeDirectory(String),
}
impl Publish {
/// Executes `wasmer publish`
pub fn execute(&self) -> Result<(), anyhow::Error> {
let mut builder = Builder::new(Vec::new());
let cwd = match self.package_path.as_ref() {
Some(s) => std::env::current_dir()?.join(s),
None => std::env::current_dir()?,
};
let manifest_path_buf = cwd.join("wasmer.toml");
let manifest = std::fs::read_to_string(&manifest_path_buf)
.map_err(|e| anyhow::anyhow!("could not find manifest: {e}"))
.with_context(|| anyhow::anyhow!("{}", manifest_path_buf.display()))?;
let mut manifest = wasmer_toml::Manifest::parse(&manifest)?;
manifest.base_directory_path = cwd.clone();
if let Some(package_name) = self.package_name.as_ref() {
manifest.package.name = package_name.to_string();
}
if let Some(version) = self.version.as_ref() {
manifest.package.version = version.clone();
}
let registry = match self.registry.as_deref() {
Some(s) => wasmer_registry::format_graphql(s),
None => {
let wasmer_dir = WasmerConfig::get_wasmer_dir()
.map_err(|e| anyhow::anyhow!("no wasmer dir: {e}"))?;
let config = WasmerConfig::from_file(&wasmer_dir)
.map_err(|e| anyhow::anyhow!("could not load config {e}"))?;
config.registry.get_current_registry()
}
};
if !self.no_validate {
validate::validate_directory(&manifest, &registry, cwd.clone())?;
}
builder.append_path_with_name(&manifest_path_buf, PACKAGE_TOML_FALLBACK_NAME)?;
let manifest_string = toml::to_string(&manifest)?;
let (readme, license) = construct_tar_gz(&mut builder, &manifest, &cwd)?;
builder
.finish()
.map_err(|e| anyhow::anyhow!("failed to finish .tar.gz builder: {e}"))?;
let tar_archive_data = builder.into_inner().map_err(|_| PublishError::NoModule)?;
let archive_name = "package.tar.gz".to_string();
let archive_dir = tempfile::TempDir::new()?;
let archive_dir_path: &std::path::Path = archive_dir.as_ref();
fs::create_dir(archive_dir_path.join("wapm_package"))?;
let archive_path = archive_dir_path.join("wapm_package").join(&archive_name);
let mut compressed_archive = fs::File::create(&archive_path).unwrap();
let mut gz_enc = GzEncoder::new(&mut compressed_archive, Compression::best());
gz_enc.write_all(&tar_archive_data).unwrap();
let _compressed_archive = gz_enc.finish().unwrap();
let mut compressed_archive_reader = fs::File::open(&archive_path)?;
let maybe_signature_data = sign_compressed_archive(&mut compressed_archive_reader)?;
let archived_data_size = archive_path.metadata()?.len();
assert!(archive_path.exists());
assert!(archive_path.is_file());
if self.dry_run {
// dry run: publish is done here
println!(
"Successfully published package `{}@{}`",
manifest.package.name, manifest.package.version
);
log::info!(
"Publish succeeded, but package was not published because it was run in dry-run mode"
);
return Ok(());
}
wasmer_registry::publish::try_chunked_uploading(
Some(registry),
self.token.clone(),
&manifest.package,
&manifest_string,
&license,
&readme,
&archive_name,
&archive_path,
&maybe_signature_data,
archived_data_size,
self.quiet,
)
.map_err(on_error)
}
}
fn construct_tar_gz(
builder: &mut tar::Builder<Vec<u8>>,
manifest: &wasmer_toml::Manifest,
cwd: &Path,
) -> Result<(Option<String>, Option<String>), anyhow::Error> {
let package = &manifest.package;
let modules = manifest.module.as_ref().ok_or(PublishError::NoModule)?;
let readme = match package.readme.as_ref() {
None => None,
Some(s) => {
let path = append_path_to_tar_gz(builder, &manifest.base_directory_path, s).map_err(
|(p, e)| PublishError::ErrorBuildingPackage(format!("{}", p.display()), e),
)?;
fs::read_to_string(path).ok()
}
};
let license_file = match package.license_file.as_ref() {
None => None,
Some(s) => {
let path = append_path_to_tar_gz(builder, &manifest.base_directory_path, s).map_err(
|(p, e)| PublishError::ErrorBuildingPackage(format!("{}", p.display()), e),
)?;
fs::read_to_string(path).ok()
}
};
for module in modules {
append_path_to_tar_gz(builder, &manifest.base_directory_path, &module.source).map_err(
|(normalized_path, _)| PublishError::SourceMustBeFile {
module: module.name.clone(),
path: normalized_path,
},
)?;
if let Some(bindings) = &module.bindings {
for path in bindings.referenced_files(&manifest.base_directory_path)? {
append_path_to_tar_gz(builder, &manifest.base_directory_path, &path).map_err(
|(normalized_path, _)| PublishError::MissingBindings {
module: module.name.clone(),
path: normalized_path,
},
)?;
}
}
}
// bundle the package filesystem
let default = std::collections::HashMap::default();
for (_alias, path) in manifest.fs.as_ref().unwrap_or(&default).iter() {
let normalized_path = normalize_path(cwd, path);
let path_metadata = normalized_path.metadata().map_err(|_| {
PublishError::MissingManifestFsPath(normalized_path.to_string_lossy().to_string())
})?;
if path_metadata.is_dir() {
builder.append_dir_all(path, &normalized_path)
} else {
return Err(PublishError::PackageFileSystemEntryMustBeDirectory(
path.to_string_lossy().to_string(),
)
.into());
}
.map_err(|_| {
PublishError::MissingManifestFsPath(normalized_path.to_string_lossy().to_string())
})?;
}
Ok((readme, license_file))
}
fn append_path_to_tar_gz(
builder: &mut tar::Builder<Vec<u8>>,
base_path: &Path,
target_path: &Path,
) -> Result<PathBuf, (PathBuf, io::Error)> {
let normalized_path = normalize_path(base_path, target_path);
normalized_path
.metadata()
.map_err(|e| (normalized_path.clone(), e))?;
builder
.append_path_with_name(&normalized_path, &target_path)
.map_err(|e| (normalized_path.clone(), e))?;
Ok(normalized_path)
}
fn on_error(e: anyhow::Error) -> anyhow::Error {
#[cfg(feature = "telemetry")]
sentry::integrations::anyhow::capture_anyhow(&e);
e
}
fn normalize_path(cwd: &Path, path: &Path) -> PathBuf {
let mut out = PathBuf::from(cwd);
let mut components = path.components();
if path.is_absolute() {
log::warn!(
"Interpreting absolute path {} as a relative path",
path.to_string_lossy()
);
components.next();
}
for comp in components {
out.push(comp);
}
out
}
/// Takes the package archive as a File and attempts to sign it using the active key
/// returns the public key id used to sign it and the signature string itself
pub fn sign_compressed_archive(
compressed_archive: &mut fs::File,
) -> anyhow::Result<SignArchiveResult> {
let key_db = open_db()?;
let personal_key = if let Ok(v) = get_active_personal_key(&key_db) {
v
} else {
return Ok(SignArchiveResult::NoKeyRegistered);
};
let password = rpassword::prompt_password(format!(
"Please enter your password for the key pair {}:",
&personal_key.public_key_id
))
.ok();
let private_key = if let Some(priv_key_location) = personal_key.private_key_location {
match minisign::SecretKey::from_file(&priv_key_location, password) {
Ok(priv_key_data) => priv_key_data,
Err(e) => {
log::error!(
"Could not read private key from location {}: {}",
priv_key_location,
e
);
return Err(e.into());
}
}
} else {
// TODO: add more info about why this might have happened and what the user can do about it
log::warn!("Active key does not have a private key location registered with it!");
return Err(anyhow!("Cannot sign package, no private key"));
};
let public_key = minisign::PublicKey::from_base64(&personal_key.public_key_value)?;
let signature = minisign::sign(
Some(&public_key),
&private_key,
compressed_archive,
None,
None,
)?;
Ok(SignArchiveResult::Ok {
public_key_id: personal_key.public_key_id,
signature: signature.to_string(),
})
}
/// Opens an exclusive read/write connection to the database, creating it if it does not exist
pub fn open_db() -> anyhow::Result<Connection> {
let wasmer_dir =
WasmerConfig::get_wasmer_dir().map_err(|e| anyhow::anyhow!("no wasmer dir: {e}"))?;
let db_path = WasmerConfig::get_database_file_path(&wasmer_dir);
let mut conn = Connection::open_with_flags(
db_path,
OpenFlags::SQLITE_OPEN_CREATE
| OpenFlags::SQLITE_OPEN_READ_WRITE
| OpenFlags::SQLITE_OPEN_FULL_MUTEX,
)?;
apply_migrations(&mut conn)?;
Ok(conn)
}
/// Applies migrations to the database
pub fn apply_migrations(conn: &mut Connection) -> anyhow::Result<()> {
let user_version = conn.pragma_query_value(None, "user_version", |val| val.get(0))?;
for data_version in user_version..CURRENT_DATA_VERSION {
log::debug!("Applying migration {}", data_version);
apply_migration(conn, data_version as i32)?;
}
Ok(())
}
#[derive(Debug, Error)]
enum MigrationError {
#[error(
"Critical internal error: the data version {0} is not handleded; current data version: {1}"
)]
MigrationNumberDoesNotExist(i32, i32),
#[error("Critical internal error: failed to commit trasaction migrating to data version {0}")]
CommitFailed(i32),
#[error("Critical internal error: transaction failed on migration number {0}: {1}")]
TransactionFailed(i32, String),
}
/// Applies migrations to the database and updates the `user_version` pragma.
/// Every migration must leave the database in a valid state.
fn apply_migration(conn: &mut Connection, migration_number: i32) -> Result<(), MigrationError> {
let tx = conn
.transaction_with_behavior(TransactionBehavior::Immediate)
.map_err(|e| MigrationError::TransactionFailed(migration_number, format!("{}", e)))?;
let migration_to_apply = MIGRATIONS
.iter()
.find_map(|(number, sql)| {
if *number == migration_number {
Some(sql)
} else {
None
}
})
.ok_or({
MigrationError::MigrationNumberDoesNotExist(
migration_number,
CURRENT_DATA_VERSION as i32,
)
})?;
tx.execute_batch(migration_to_apply)
.map_err(|e| MigrationError::TransactionFailed(migration_number, format!("{}", e)))?;
tx.pragma_update(None, "user_version", &(migration_number + 1))
.map_err(|e| MigrationError::TransactionFailed(migration_number, format!("{}", e)))?;
tx.commit()
.map_err(|_| MigrationError::CommitFailed(migration_number))
}
/// Information about one of the user's keys
#[derive(Debug)]
pub struct PersonalKey {
/// Flag saying if the key will be used (there can only be one active key at a time)
pub active: bool,
/// The public key's tag. Used to identify the key pair
pub public_key_id: String,
/// The raw value of the public key in base64
pub public_key_value: String,
/// The location in the file system of the private key
pub private_key_location: Option<String>,
/// The type of private/public key this is
pub key_type_identifier: String,
/// The time at which the key was registered with wapm
pub date_created: OffsetDateTime,
}
fn get_active_personal_key(conn: &Connection) -> anyhow::Result<PersonalKey> {
let mut stmt = conn.prepare(
"SELECT active, public_key_value, private_key_location, date_added, key_type_identifier, public_key_id FROM personal_keys
where active = 1",
)?;
let result = stmt
.query_map(params![], |row| {
Ok(PersonalKey {
active: row.get(0)?,
public_key_value: row.get(1)?,
private_key_location: row.get(2)?,
date_created: {
use time::format_description::well_known::Rfc3339;
let time_str: String = row.get(3)?;
OffsetDateTime::parse(&time_str, &Rfc3339)
.unwrap_or_else(|_| panic!("Failed to parse time string {}", &time_str))
},
key_type_identifier: row.get(4)?,
public_key_id: row.get(5)?,
})
})?
.next();
if let Some(res) = result {
Ok(res?)
} else {
Err(anyhow!("No active key found"))
}
}
mod interfaces {
use rusqlite::{params, Connection, TransactionBehavior};
pub const WASM_INTERFACE_EXISTENCE_CHECK: &str =
include_str!("./sql/wasm_interface_existence_check.sql");
pub const INSERT_WASM_INTERFACE: &str = include_str!("./sql/insert_interface.sql");
pub const GET_WASM_INTERFACE: &str = include_str!("./sql/get_interface.sql");
pub fn interface_exists(
conn: &mut Connection,
interface_name: &str,
version: &str,
) -> anyhow::Result<bool> {
let mut stmt = conn.prepare(WASM_INTERFACE_EXISTENCE_CHECK)?;
Ok(stmt.exists(params![interface_name, version])?)
}
pub fn load_interface_from_db(
conn: &mut Connection,
interface_name: &str,
version: &str,
) -> anyhow::Result<wasmer_wasm_interface::Interface> {
let mut stmt = conn.prepare(GET_WASM_INTERFACE)?;
let interface_string: String =
stmt.query_row(params![interface_name, version], |row| row.get(0))?;
wasmer_wasm_interface::parser::parse_interface(&interface_string).map_err(|e| {
anyhow!(
"Failed to parse interface {} version {} in database: {}",
interface_name,
version,
e
)
})
}
pub fn import_interface(
conn: &mut Connection,
interface_name: &str,
version: &str,
content: &str,
) -> anyhow::Result<()> {
// fail if we already have this interface
{
let mut key_check = conn.prepare(WASM_INTERFACE_EXISTENCE_CHECK)?;
let result = key_check.exists(params![interface_name, version])?;
if result {
return Err(anyhow!(
"Interface {}, version {} already exists",
interface_name,
version
));
}
}
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
let time_string = get_current_time_in_format().expect("Could not get current time");
log::debug!("Adding interface {:?} {:?}", interface_name, version);
tx.execute(
INSERT_WASM_INTERFACE,
params![interface_name, version, time_string, content],
)?;
tx.commit()?;
Ok(())
}
/// Gets the current time in our standard format
pub fn get_current_time_in_format() -> Option<String> {
use time::format_description::well_known::Rfc3339;
let cur_time = time::OffsetDateTime::now_utc();
cur_time.format(&Rfc3339).ok()
}
}
mod validate {
use super::interfaces;
use std::{
fs,
io::Read,
path::{Path, PathBuf},
};
use thiserror::Error;
use wasmer_registry::interface::InterfaceFromServer;
use wasmer_wasm_interface::{validate, Interface};
pub fn validate_directory(
manifest: &wasmer_toml::Manifest,
registry: &str,
pkg_path: PathBuf,
) -> anyhow::Result<()> {
// validate as dir
if let Some(modules) = manifest.module.as_ref() {
for module in modules.iter() {
let source_path = if module.source.is_relative() {
manifest.base_directory_path.join(&module.source)
} else {
module.source.clone()
};
let source_path_string = source_path.to_string_lossy().to_string();
let mut wasm_file =
fs::File::open(&source_path).map_err(|_| ValidationError::MissingFile {
file: source_path_string.clone(),
})?;
let mut wasm_buffer = Vec::new();
wasm_file.read_to_end(&mut wasm_buffer).map_err(|err| {
ValidationError::MiscCannotRead {
file: source_path_string.clone(),
error: format!("{}", err),
}
})?;
if let Some(bindings) = &module.bindings {
validate_bindings(bindings, &manifest.base_directory_path)?;
}
// hack, short circuit if no interface for now
if module.interfaces.is_none() {
return validate_wasm_and_report_errors_old(
&wasm_buffer[..],
source_path_string,
);
}
let mut conn = super::open_db()?;
let mut interface: Interface = Default::default();
for (interface_name, interface_version) in
module.interfaces.clone().unwrap_or_default().into_iter()
{
if !interfaces::interface_exists(
&mut conn,
&interface_name,
&interface_version,
)? {
// download interface and store it if we don't have it locally
let interface_data_from_server = InterfaceFromServer::get(
registry,
interface_name.clone(),
interface_version.clone(),
)?;
interfaces::import_interface(
&mut conn,
&interface_name,
&interface_version,
&interface_data_from_server.content,
)?;
}
let sub_interface = interfaces::load_interface_from_db(
&mut conn,
&interface_name,
&interface_version,
)?;
interface = interface.merge(sub_interface).map_err(|e| {
anyhow!("Failed to merge interface {}: {}", &interface_name, e)
})?;
}
validate::validate_wasm_and_report_errors(&wasm_buffer, &interface).map_err(
|e| ValidationError::InvalidWasm {
file: source_path_string,
error: format!("{:?}", e),
},
)?;
}
}
log::debug!("package at path {:#?} validated", &pkg_path);
Ok(())
}
fn validate_bindings(
bindings: &wasmer_toml::Bindings,
base_directory_path: &Path,
) -> Result<(), ValidationError> {
// Note: checking for referenced files will make sure they all exist.
let _ = bindings.referenced_files(base_directory_path)?;
Ok(())
}
#[derive(Debug, Error)]
pub enum ValidationError {
#[error("WASM file \"{file}\" detected as invalid because {error}")]
InvalidWasm { file: String, error: String },
#[error("Could not find file {file}")]
MissingFile { file: String },
#[error("Failed to read file {file}; {error}")]
MiscCannotRead { file: String, error: String },
#[error(transparent)]
Imports(#[from] wasmer_toml::ImportsError),
}
// legacy function, validates wasm. TODO: clean up
pub fn validate_wasm_and_report_errors_old(
wasm: &[u8],
file_name: String,
) -> anyhow::Result<()> {
use wasmparser::WasmDecoder;
let mut parser = wasmparser::ValidatingParser::new(wasm, None);
loop {
let state = parser.read();
match state {
wasmparser::ParserState::EndWasm => return Ok(()),
wasmparser::ParserState::Error(e) => {
return Err(ValidationError::InvalidWasm {
file: file_name,
error: format!("{}", e),
}
.into());
}
_ => {}
}
}
}
}

View File

@ -0,0 +1,4 @@
SELECT content
FROM wasm_interfaces
WHERE interface_name = (?1)
AND version = (?2)

View File

@ -0,0 +1,3 @@
INSERT INTO wasm_interfaces
(interface_name, version, date_added, content)
VALUES (?1, ?2, ?3, ?4)

View File

@ -0,0 +1,5 @@
SELECT 1
FROM wasm_interfaces
WHERE interface_name = (?1)
AND version = (?2)
LIMIT 1

View File

@ -1,4 +1,5 @@
use clap::Parser; use clap::Parser;
use wasmer_registry::WasmerConfig;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
/// The options for the `wasmer whoami` subcommand /// The options for the `wasmer whoami` subcommand
@ -11,7 +12,10 @@ pub struct Whoami {
impl Whoami { impl Whoami {
/// Execute `wasmer whoami` /// Execute `wasmer whoami`
pub fn execute(&self) -> Result<(), anyhow::Error> { pub fn execute(&self) -> Result<(), anyhow::Error> {
let (registry, username) = wasmer_registry::whoami(self.registry.as_deref())?; let wasmer_dir =
WasmerConfig::get_wasmer_dir().map_err(|e| anyhow::anyhow!("no wasmer dir: {e}"))?;
let (registry, username) =
wasmer_registry::whoami(&wasmer_dir, self.registry.as_deref(), None)?;
println!("logged into registry {registry:?} as user {username:?}"); println!("logged into registry {registry:?} as user {username:?}");
Ok(()) Ok(())
} }

View File

@ -4,6 +4,7 @@ use anyhow::Context;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use url::Url; use url::Url;
use wasmer_registry::WasmerConfig;
/// Source of a package /// Source of a package
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
@ -47,7 +48,7 @@ impl PackageSource {
} }
/// Downloads the package (if any) to the installation directory, returns the path /// Downloads the package (if any) to the installation directory, returns the path
/// of the package directory (containing the wapm.toml) /// of the package directory (containing the wasmer.toml)
pub fn download_and_get_filepath(&self) -> Result<PathBuf, anyhow::Error> { pub fn download_and_get_filepath(&self) -> Result<PathBuf, anyhow::Error> {
let url = match self { let url = match self {
Self::File(f) => { Self::File(f) => {
@ -61,20 +62,28 @@ impl PackageSource {
}; };
} }
Self::Url(u) => { Self::Url(u) => {
if let Some(path) = wasmer_registry::Package::is_url_already_installed(u) { let wasmer_dir = WasmerConfig::get_wasmer_dir()
.map_err(|e| anyhow::anyhow!("no wasmer dir: {e}"))?;
if let Some(path) =
wasmer_registry::Package::is_url_already_installed(u, &wasmer_dir)
{
return Ok(path); return Ok(path);
} else { } else {
u.clone() u.clone()
} }
} }
Self::Package(p) => { Self::Package(p) => {
let wasmer_dir = WasmerConfig::get_wasmer_dir()
.map_err(|e| anyhow::anyhow!("no wasmer dir: {e}"))?;
let package_path = Path::new(&p.file()).to_path_buf(); let package_path = Path::new(&p.file()).to_path_buf();
if package_path.exists() { if package_path.exists() {
return Ok(package_path); return Ok(package_path);
} else if let Some(path) = p.already_installed() { } else if let Some(path) = p.already_installed(&wasmer_dir) {
return Ok(path); return Ok(path);
} else { } else {
p.url()? let config = WasmerConfig::from_file(&wasmer_dir)
.map_err(|e| anyhow::anyhow!("error loading wasmer config file: {e}"))?;
p.url(&config.registry.get_current_registry())?
} }
} }
}; };
@ -85,8 +94,10 @@ impl PackageSource {
String::new() String::new()
}; };
let wasmer_dir =
WasmerConfig::get_wasmer_dir().map_err(|e| anyhow::anyhow!("no wasmer dir: {e}"))?;
let mut sp = start_spinner(format!("Installing package {url} ...")); let mut sp = start_spinner(format!("Installing package {url} ..."));
let opt_path = wasmer_registry::install_package(&url); let opt_path = wasmer_registry::install_package(&wasmer_dir, &url);
if let Some(sp) = sp.take() { if let Some(sp) = sp.take() {
use std::io::Write; use std::io::Write;
sp.clear(); sp.clear();

View File

@ -38,4 +38,3 @@ wasm = ["std", "unwind"]
unwind = ["cranelift-codegen/unwind", "gimli"] unwind = ["cranelift-codegen/unwind", "gimli"]
std = ["cranelift-codegen/std", "cranelift-frontend/std", "wasmer-compiler/std", "wasmer-types/std"] std = ["cranelift-codegen/std", "cranelift-frontend/std", "wasmer-compiler/std", "wasmer-types/std"]
core = ["hashbrown", "cranelift-codegen/core", "cranelift-frontend/core"] core = ["hashbrown", "cranelift-codegen/core", "cranelift-frontend/core"]
verbose = []

View File

@ -79,8 +79,7 @@ impl FuncTranslator {
environ: &mut FE, environ: &mut FE,
) -> WasmResult<()> { ) -> WasmResult<()> {
let _tt = timing::wasm_translate_function(); let _tt = timing::wasm_translate_function();
#[cfg(feature = "verbose")] tracing::trace!(
tracing::info!(
"translate({} bytes, {}{})", "translate({} bytes, {}{})",
reader.bytes_remaining(), reader.bytes_remaining(),
func.name, func.name,

View File

@ -60,7 +60,10 @@ impl UnwindRegistry {
#[allow(clippy::cast_ptr_alignment)] #[allow(clippy::cast_ptr_alignment)]
unsafe fn register_frames(&mut self, eh_frame: &[u8]) { unsafe fn register_frames(&mut self, eh_frame: &[u8]) {
if cfg!(all(target_os = "linux", target_env = "gnu")) { if cfg!(any(
all(target_os = "linux", target_env = "gnu"),
target_os = "freebsd"
)) {
// Registering an empty `eh_frame` (i.e. which // Registering an empty `eh_frame` (i.e. which
// contains empty FDEs) cause problems on Linux when // contains empty FDEs) cause problems on Linux when
// deregistering it. We must avoid this // deregistering it. We must avoid this

View File

@ -1,6 +1,6 @@
[package] [package]
name = "wasmer-registry" name = "wasmer-registry"
version = "3.1.0" version = "4.0.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "Crate to interact with the wasmer registry (wapm.io), download packages, etc." description = "Crate to interact with the wasmer registry (wapm.io), download packages, etc."
@ -20,12 +20,12 @@ serde_json = "1.0.85"
url = "2.3.1" url = "2.3.1"
thiserror = "1.0.37" thiserror = "1.0.37"
toml = "0.5.9" toml = "0.5.9"
wapm-toml = "0.2.0" wasmer-toml = "0.5.0"
tar = "0.4.38" tar = "0.4.38"
flate2 = "1.0.24" flate2 = "1.0.24"
semver = "1.0.14" semver = "1.0.14"
lzma-rs = "0.2.0" lzma-rs = "0.2.0"
webc = { version ="0.4.1", features = ["mmap"] } webc = { version ="4.0.0", features = ["mmap"] }
hex = "0.4.3" hex = "0.4.3"
tokio = "1.21.2" tokio = "1.21.2"
tempdir = "0.3.7" tempdir = "0.3.7"
@ -34,3 +34,6 @@ regex = "1.7.0"
fs_extra = "1.2.0" fs_extra = "1.2.0"
filetime = "0.2.19" filetime = "0.2.19"
tldextract = "0.6.0" tldextract = "0.6.0"
console = "0.15.2"
indicatif = "0.17.2"
lazy_static = "1.4.0"

View File

@ -0,0 +1,9 @@
query GetInterfaceVersionQuery ($name: String!, $version: String!) {
interface: getInterfaceVersion(name: $name, version: $version) {
version,
content,
interface {
name,
}
}
}

View File

@ -0,0 +1,5 @@
query GetSignedUrl ($name:String!, $version:String!,$expiresAfterSeconds:Int) {
url: getSignedUrlForPackageUpload(name:$name, version:$version,expiresAfterSeconds:$expiresAfterSeconds) {
url
}
}

View File

@ -0,0 +1,22 @@
mutation PublishPackageMutationChunked($name: String!, $version: String!, $description: String!, $manifest: String!, $license: String, $licenseFile: String, $readme: String, $fileName:String, $repository:String, $homepage:String, $signature: InputSignature, $signedUrl:String) {
publishPackage(input: {
name: $name,
version: $version,
description: $description,
manifest: $manifest,
license: $license,
licenseFile: $licenseFile,
readme: $readme,
file: $fileName,
signedUrl: $signedUrl,
repository: $repository,
homepage: $homepage,
signature: $signature,
clientMutationId: ""
}) {
success
packageVersion {
version
}
}
}

View File

@ -1,26 +1,21 @@
use graphql_client::GraphQLQuery; use graphql_client::GraphQLQuery;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use serde::Serialize;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
pub static GLOBAL_CONFIG_DATABASE_FILE_NAME: &str = "wasmer.sqlite";
#[derive(Deserialize, Default, Serialize, Debug, PartialEq, Eq)] #[derive(Deserialize, Default, Serialize, Debug, PartialEq, Eq)]
pub struct PartialWapmConfig { pub struct WasmerConfig {
/// The number of seconds to wait before checking the registry for a new
/// version of the package.
#[serde(default = "wax_default_cooldown")]
pub wax_cooldown: i32,
/// The registry that wapm will connect to.
pub registry: Registries,
/// Whether or not telemetry is enabled. /// Whether or not telemetry is enabled.
#[serde(default)] #[serde(default)]
pub telemetry: Telemetry, pub telemetry_enabled: bool,
/// Whether or not updated notifications are enabled. /// Whether or not updated notifications are enabled.
#[serde(default)] #[serde(default)]
pub update_notifications: UpdateNotifications, pub update_notifications_enabled: bool,
/// The registry that wapm will connect to.
pub registry: MultiRegistry,
/// The proxy to use when connecting to the Internet. /// The proxy to use when connecting to the Internet.
#[serde(default)] #[serde(default)]
@ -36,38 +31,31 @@ pub struct Proxy {
pub url: Option<String>, pub url: Option<String>,
} }
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Default)] /// Struct to store login tokens for multiple registry URLs
pub struct UpdateNotifications { /// inside of the wasmer.toml configuration file
pub enabled: String,
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
pub struct Telemetry {
pub enabled: String,
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
#[serde(untagged)]
pub enum Registries {
Single(Registry),
Multi(MultiRegistry),
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] #[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
pub struct MultiRegistry { pub struct MultiRegistry {
/// Currently active registry /// Currently active registry
pub current: String, pub active_registry: String,
/// Map from "RegistryUrl" to "LoginToken", in order to /// Map from "RegistryUrl" to "LoginToken", in order to
/// be able to be able to easily switch between registries /// be able to be able to easily switch between registries
pub tokens: BTreeMap<String, String>, pub tokens: Vec<RegistryLogin>,
} }
impl Default for Registries { #[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
pub struct RegistryLogin {
/// Registry URL to login to
pub registry: String,
/// Login token for the registry
pub token: String,
}
impl Default for MultiRegistry {
fn default() -> Self { fn default() -> Self {
Registries::Single(Registry { MultiRegistry {
url: format_graphql("https://registry.wapm.io"), active_registry: format_graphql("wapm.io"),
token: None, tokens: Vec::new(),
}) }
} }
} }
@ -110,7 +98,7 @@ pub enum UpdateRegistry {
#[test] #[test]
fn test_registries_switch_token() { fn test_registries_switch_token() {
let mut registries = Registries::default(); let mut registries = MultiRegistry::default();
registries.set_current_registry("https://registry.wapm.dev"); registries.set_current_registry("https://registry.wapm.dev");
assert_eq!( assert_eq!(
@ -142,18 +130,12 @@ fn test_registries_switch_token() {
); );
} }
impl Registries { impl MultiRegistry {
/// Gets the current (active) registry URL /// Gets the current (active) registry URL
pub fn clear_current_registry_token(&mut self) { pub fn clear_current_registry_token(&mut self) {
match self { self.tokens.retain(|i| i.registry != self.active_registry);
Registries::Single(s) => { self.tokens
s.token = None; .retain(|i| i.registry != format_graphql(&self.active_registry));
}
Registries::Multi(m) => {
m.tokens.remove(&m.current);
m.tokens.remove(&format_graphql(&m.current));
}
}
} }
pub fn get_graphql_url(&self) -> String { pub fn get_graphql_url(&self) -> String {
@ -163,10 +145,7 @@ impl Registries {
/// Gets the current (active) registry URL /// Gets the current (active) registry URL
pub fn get_current_registry(&self) -> String { pub fn get_current_registry(&self) -> String {
match self { format_graphql(&self.active_registry)
Registries::Single(s) => format_graphql(&s.url),
Registries::Multi(m) => format_graphql(&m.current),
}
} }
/// Sets the current (active) registry URL /// Sets the current (active) registry URL
@ -176,25 +155,17 @@ impl Registries {
println!("Error when trying to ping registry {registry:?}: {e}"); println!("Error when trying to ping registry {registry:?}: {e}");
println!("WARNING: Registry {registry:?} will be used, but commands may not succeed."); println!("WARNING: Registry {registry:?} will be used, but commands may not succeed.");
} }
match self { self.active_registry = registry;
Registries::Single(s) => s.url = registry,
Registries::Multi(m) => m.current = registry,
}
} }
/// Returns the login token for the registry /// Returns the login token for the registry
pub fn get_login_token_for_registry(&self, registry: &str) -> Option<String> { pub fn get_login_token_for_registry(&self, registry: &str) -> Option<String> {
match self { let registry_formatted = format_graphql(registry);
Registries::Single(s) if s.url == registry || format_graphql(registry) == s.url => { self.tokens
s.token.clone() .iter()
} .filter(|login| login.registry == registry || login.registry == registry_formatted)
Registries::Multi(m) => m .last()
.tokens .map(|login| login.token.clone())
.get(registry)
.or_else(|| m.tokens.get(&format_graphql(registry)))
.cloned(),
_ => None,
}
} }
/// Sets the login token for the registry URL /// Sets the login token for the registry URL
@ -204,38 +175,20 @@ impl Registries {
token: &str, token: &str,
update_current_registry: UpdateRegistry, update_current_registry: UpdateRegistry,
) { ) {
let new_map = match self { let registry_formatted = format_graphql(registry);
Registries::Single(s) => { self.tokens
if s.url == registry { .retain(|login| !(login.registry == registry || login.registry == registry_formatted));
Registries::Single(Registry { self.tokens.push(RegistryLogin {
url: format_graphql(registry), registry: format_graphql(registry),
token: Some(token.to_string()), token: token.to_string(),
}) });
} else { if update_current_registry == UpdateRegistry::Update {
let mut map = BTreeMap::new(); self.active_registry = format_graphql(registry);
if let Some(token) = s.token.clone() { }
map.insert(format_graphql(&s.url), token);
}
map.insert(format_graphql(registry), token.to_string());
Registries::Multi(MultiRegistry {
current: format_graphql(&s.url),
tokens: map,
})
}
}
Registries::Multi(m) => {
m.tokens.insert(format_graphql(registry), token.to_string());
if update_current_registry == UpdateRegistry::Update {
m.current = format_graphql(registry);
}
Registries::Multi(m.clone())
}
};
*self = new_map;
} }
} }
impl PartialWapmConfig { impl WasmerConfig {
/// Save the config to a file /// Save the config to a file
pub fn save<P: AsRef<Path>>(&self, to: P) -> anyhow::Result<()> { pub fn save<P: AsRef<Path>>(&self, to: P) -> anyhow::Result<()> {
use std::{fs::File, io::Write}; use std::{fs::File, io::Write};
@ -245,33 +198,16 @@ impl PartialWapmConfig {
Ok(()) Ok(())
} }
pub fn from_file(#[cfg(test)] test_name: &str) -> Result<Self, String> { pub fn from_file(wasmer_dir: &Path) -> Result<Self, String> {
#[cfg(test)] let path = Self::get_file_location(wasmer_dir);
let path = Self::get_file_location(test_name)?;
#[cfg(not(test))]
let path = Self::get_file_location()?;
match std::fs::read_to_string(&path) { match std::fs::read_to_string(&path) {
Ok(config_toml) => { Ok(config_toml) => Ok(toml::from_str(&config_toml).unwrap_or_else(|_| Self::default())),
toml::from_str(&config_toml).map_err(|e| format!("could not parse {path:?}: {e}"))
}
Err(_e) => Ok(Self::default()), Err(_e) => Ok(Self::default()),
} }
} }
pub fn get_current_dir() -> std::io::Result<PathBuf> { /// Creates and returns the `WASMER_DIR` directory (or $HOME/.wasmer as a fallback)
std::env::current_dir() pub fn get_wasmer_dir() -> Result<PathBuf, String> {
}
#[cfg(test)]
pub fn get_folder(test_name: &str) -> Result<PathBuf, String> {
let test_dir = std::env::temp_dir().join("test_wasmer").join(test_name);
let _ = std::fs::create_dir_all(&test_dir);
Ok(test_dir)
}
#[cfg(not(test))]
pub fn get_folder() -> Result<PathBuf, String> {
Ok( Ok(
if let Some(folder_str) = std::env::var("WASMER_DIR").ok().filter(|s| !s.is_empty()) { if let Some(folder_str) = std::env::var("WASMER_DIR").ok().filter(|s| !s.is_empty()) {
let folder = PathBuf::from(folder_str); let folder = PathBuf::from(folder_str);
@ -294,14 +230,16 @@ impl PartialWapmConfig {
) )
} }
#[cfg(test)] pub fn get_current_dir() -> std::io::Result<PathBuf> {
pub fn get_file_location(test_name: &str) -> Result<PathBuf, String> { std::env::current_dir()
Ok(Self::get_folder(test_name)?.join(crate::GLOBAL_CONFIG_FILE_NAME))
} }
#[cfg(not(test))] pub fn get_file_location(wasmer_dir: &Path) -> PathBuf {
pub fn get_file_location() -> Result<PathBuf, String> { wasmer_dir.join(crate::GLOBAL_CONFIG_FILE_NAME)
Ok(Self::get_folder()?.join(crate::GLOBAL_CONFIG_FILE_NAME)) }
pub fn get_database_file_path(wasmer_dir: &Path) -> PathBuf {
wasmer_dir.join(GLOBAL_CONFIG_DATABASE_FILE_NAME)
} }
} }

View File

@ -99,6 +99,60 @@ pub(crate) mod proxy {
} }
} }
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.graphql",
query_path = "graphql/queries/get_package_version.graphql",
response_derives = "Debug"
)]
pub(crate) struct GetPackageVersionQuery;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.graphql",
query_path = "graphql/queries/get_package_by_command.graphql",
response_derives = "Debug"
)]
pub(crate) struct GetPackageByCommandQuery;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.graphql",
query_path = "graphql/queries/test_if_registry_present.graphql",
response_derives = "Debug"
)]
pub(crate) struct TestIfRegistryPresent;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.graphql",
query_path = "graphql/queries/get_bindings.graphql",
response_derives = "Debug,Clone,PartialEq,Eq"
)]
pub(crate) struct GetBindingsQuery;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.graphql",
query_path = "graphql/queries/publish_package_chunked.graphql",
response_derives = "Debug"
)]
pub(crate) struct PublishPackageMutationChunked;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.graphql",
query_path = "graphql/queries/get_signed_url.graphql",
response_derives = "Debug, Clone"
)]
pub(crate) struct GetSignedUrl;
#[cfg(target_os = "wasi")]
pub fn whoami_distro() -> String {
whoami::os().to_lowercase()
}
#[cfg(not(target_os = "wasi"))]
pub fn whoami_distro() -> String { pub fn whoami_distro() -> String {
whoami::distro().to_lowercase() whoami::distro().to_lowercase()
} }

View File

@ -0,0 +1,47 @@
#![cfg_attr(
not(feature = "full"),
allow(dead_code, unused_imports, unused_variables)
)]
use crate::graphql::execute_query;
use graphql_client::*;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.graphql",
query_path = "graphql/queries/get_interface_version.graphql",
response_derives = "Debug"
)]
struct GetInterfaceVersionQuery;
#[derive(Debug)]
pub struct InterfaceFromServer {
pub name: String,
pub version: String,
pub content: String,
}
impl InterfaceFromServer {
fn get_response(
registry: &str,
name: String,
version: String,
) -> anyhow::Result<get_interface_version_query::ResponseData> {
let q = GetInterfaceVersionQuery::build_query(get_interface_version_query::Variables {
name,
version,
});
execute_query(registry, "", &q)
}
pub fn get(registry: &str, name: String, version: String) -> anyhow::Result<Self> {
let response = Self::get_response(registry, name, version)?;
let response_val = response
.interface
.ok_or_else(|| anyhow::anyhow!("Error downloading Interface from the server"))?;
Ok(Self {
name: response_val.interface.name,
version: response_val.version,
content: response_val.content,
})
}
}

View File

@ -8,7 +8,6 @@
//! curl -sSfL https://registry.wapm.io/graphql/schema.graphql > lib/registry/graphql/schema.graphql //! curl -sSfL https://registry.wapm.io/graphql/schema.graphql > lib/registry/graphql/schema.graphql
//! ``` //! ```
use crate::config::Registries;
use anyhow::Context; use anyhow::Context;
use core::ops::Range; use core::ops::Range;
use reqwest::header::{ACCEPT, RANGE}; use reqwest::header::{ACCEPT, RANGE};
@ -20,18 +19,22 @@ use url::Url;
pub mod config; pub mod config;
pub mod graphql; pub mod graphql;
pub mod interface;
pub mod login; pub mod login;
pub mod package; pub mod package;
pub mod publish;
pub mod queries; pub mod queries;
pub mod utils; pub mod utils;
pub use crate::{ pub use crate::{
config::{format_graphql, PartialWapmConfig}, config::{format_graphql, WasmerConfig},
package::Package, package::Package,
queries::get_bindings_query::ProgrammingLanguage, queries::get_bindings_query::ProgrammingLanguage,
}; };
pub static GLOBAL_CONFIG_FILE_NAME: &str = "wapm.toml"; pub static PACKAGE_TOML_FILE_NAME: &str = "wasmer.toml";
pub static PACKAGE_TOML_FALLBACK_NAME: &str = "wapm.toml";
pub static GLOBAL_CONFIG_FILE_NAME: &str = "wasmer.toml";
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
pub struct PackageDownloadInfo { pub struct PackageDownloadInfo {
@ -45,30 +48,17 @@ pub struct PackageDownloadInfo {
pub pirita_url: Option<String>, pub pirita_url: Option<String>,
} }
pub fn get_package_local_dir( pub fn get_package_local_dir(wasmer_dir: &Path, url: &str, version: &str) -> Option<PathBuf> {
#[cfg(test)] test_name: &str, let checkouts_dir = get_checkouts_dir(wasmer_dir);
url: &str,
version: &str,
) -> Option<PathBuf> {
#[cfg(test)]
let checkouts_dir = get_checkouts_dir(test_name)?;
#[cfg(not(test))]
let checkouts_dir = get_checkouts_dir()?;
let url_hash = Package::hash_url(url); let url_hash = Package::hash_url(url);
let dir = checkouts_dir.join(format!("{url_hash}@{version}")); let dir = checkouts_dir.join(format!("{url_hash}@{version}"));
Some(dir) Some(dir)
} }
pub fn try_finding_local_command(#[cfg(test)] test_name: &str, cmd: &str) -> Option<LocalPackage> { pub fn try_finding_local_command(wasmer_dir: &Path, cmd: &str) -> Option<LocalPackage> {
#[cfg(test)] let local_packages = get_all_local_packages(wasmer_dir);
let local_packages = get_all_local_packages(test_name);
#[cfg(not(test))]
let local_packages = get_all_local_packages();
for p in local_packages { for p in local_packages {
#[cfg(not(test))]
let commands = p.get_commands(); let commands = p.get_commands();
#[cfg(test)]
let commands = p.get_commands(test_name);
if commands.unwrap_or_default().iter().any(|c| c == cmd) { if commands.unwrap_or_default().iter().any(|c| c == cmd) {
return Some(p); return Some(p);
@ -85,20 +75,59 @@ pub struct LocalPackage {
pub path: PathBuf, pub path: PathBuf,
} }
impl fmt::Display for LocalPackage {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}@{} (registry {}, located at {})",
self.name,
self.version,
self.registry,
self.path.display()
)
}
}
impl LocalPackage { impl LocalPackage {
pub fn get_path(&self, #[cfg(test)] test_name: &str) -> Result<PathBuf, String> { pub fn get_path(&self) -> Result<PathBuf, String> {
Ok(self.path.clone()) Ok(self.path.clone())
} }
pub fn get_commands(&self, #[cfg(test)] test_name: &str) -> Result<Vec<String>, String> {
#[cfg(not(test))] /// Returns the wasmer.toml path if it exists
pub fn get_wasmer_toml_path(base_path: &Path) -> Result<PathBuf, anyhow::Error> {
let path = base_path.join(PACKAGE_TOML_FILE_NAME);
let fallback_path = base_path.join(PACKAGE_TOML_FALLBACK_NAME);
if path.exists() {
Ok(path)
} else if fallback_path.exists() {
Ok(fallback_path)
} else {
Err(anyhow::anyhow!(
"neither {} nor {} exists",
path.display(),
fallback_path.display()
))
}
}
/// Reads the wasmer.toml fron $PATH with wapm.toml as a fallback
pub fn read_toml(base_path: &Path) -> Result<wasmer_toml::Manifest, String> {
let wasmer_toml = std::fs::read_to_string(base_path.join(PACKAGE_TOML_FILE_NAME))
.or_else(|_| std::fs::read_to_string(base_path.join(PACKAGE_TOML_FALLBACK_NAME)))
.map_err(|_| {
format!(
"Path {} has no {PACKAGE_TOML_FILE_NAME} or {PACKAGE_TOML_FALLBACK_NAME}",
base_path.display()
)
})?;
let wasmer_toml = toml::from_str::<wasmer_toml::Manifest>(&wasmer_toml)
.map_err(|e| format!("Could not parse toml for {:?}: {e}", base_path.display()))?;
Ok(wasmer_toml)
}
pub fn get_commands(&self) -> Result<Vec<String>, String> {
let path = self.get_path()?; let path = self.get_path()?;
#[cfg(test)] let toml_parsed = Self::read_toml(&path)?;
let path = self.get_path(test_name)?;
let toml_path = path.join("wapm.toml");
let toml = std::fs::read_to_string(&toml_path)
.map_err(|e| format!("error reading {}: {e}", toml_path.display()))?;
let toml_parsed = toml::from_str::<wapm_toml::Manifest>(&toml)
.map_err(|e| format!("error parsing {}: {e}", toml_path.display()))?;
Ok(toml_parsed Ok(toml_parsed
.command .command
.unwrap_or_default() .unwrap_or_default()
@ -110,19 +139,15 @@ impl LocalPackage {
/// Returns the (manifest, .wasm file name), given a package dir /// Returns the (manifest, .wasm file name), given a package dir
pub fn get_executable_file_from_path( pub fn get_executable_file_from_path(
package_dir: &PathBuf, package_dir: &Path,
command: Option<&str>, command: Option<&str>,
) -> Result<(wapm_toml::Manifest, PathBuf), anyhow::Error> { ) -> Result<(wasmer_toml::Manifest, PathBuf), anyhow::Error> {
let wapm_toml = std::fs::read_to_string(package_dir.join("wapm.toml")) let wasmer_toml = LocalPackage::read_toml(package_dir).map_err(|e| anyhow::anyhow!("{e}"))?;
.map_err(|_| anyhow::anyhow!("Package {package_dir:?} has no wapm.toml"))?;
let wapm_toml = toml::from_str::<wapm_toml::Manifest>(&wapm_toml) let name = wasmer_toml.package.name.clone();
.map_err(|e| anyhow::anyhow!("Could not parse toml for {package_dir:?}: {e}"))?; let version = wasmer_toml.package.version.clone();
let name = wapm_toml.package.name.clone(); let commands = wasmer_toml.command.clone().unwrap_or_default();
let version = wapm_toml.package.version.clone();
let commands = wapm_toml.command.clone().unwrap_or_default();
let entrypoint_module = match command { let entrypoint_module = match command {
Some(s) => commands.iter().find(|c| c.get_name() == s).ok_or_else(|| { Some(s) => commands.iter().find(|c| c.get_name() == s).ok_or_else(|| {
anyhow::anyhow!("Cannot run {name}@{version}: package has no command {s:?}") anyhow::anyhow!("Cannot run {name}@{version}: package has no command {s:?}")
@ -144,19 +169,19 @@ pub fn get_executable_file_from_path(
}; };
let module_name = entrypoint_module.get_module(); let module_name = entrypoint_module.get_module();
let modules = wapm_toml.module.clone().unwrap_or_default(); let modules = wasmer_toml.module.clone().unwrap_or_default();
let entrypoint_module = modules let entrypoint_module = modules
.iter() .iter()
.find(|m| m.name == module_name) .find(|m| m.name == module_name)
.ok_or_else(|| { .ok_or_else(|| {
anyhow::anyhow!( anyhow::anyhow!(
"Cannot run {name}@{version}: module {module_name} not found in wapm.toml" "Cannot run {name}@{version}: module {module_name} not found in {GLOBAL_CONFIG_FILE_NAME}"
) )
})?; })?;
let entrypoint_source = package_dir.join(&entrypoint_module.source); let entrypoint_source = package_dir.join(&entrypoint_module.source);
Ok((wapm_toml, entrypoint_source)) Ok((wasmer_toml, entrypoint_source))
} }
fn get_all_names_in_dir(dir: &PathBuf) -> Vec<(PathBuf, String)> { fn get_all_names_in_dir(dir: &PathBuf) -> Vec<(PathBuf, String)> {
@ -185,25 +210,13 @@ fn get_all_names_in_dir(dir: &PathBuf) -> Vec<(PathBuf, String)> {
} }
/// Returns a list of all locally installed packages /// Returns a list of all locally installed packages
pub fn get_all_local_packages(#[cfg(test)] test_name: &str) -> Vec<LocalPackage> { pub fn get_all_local_packages(wasmer_dir: &Path) -> Vec<LocalPackage> {
let mut packages = Vec::new(); let mut packages = Vec::new();
#[cfg(not(test))] let checkouts_dir = get_checkouts_dir(wasmer_dir);
let checkouts_dir = get_checkouts_dir();
#[cfg(test)]
let checkouts_dir = get_checkouts_dir(test_name);
let checkouts_dir = match checkouts_dir {
Some(s) => s,
None => return packages,
};
for (path, url_hash_with_version) in get_all_names_in_dir(&checkouts_dir) { for (path, url_hash_with_version) in get_all_names_in_dir(&checkouts_dir) {
let s = match std::fs::read_to_string(path.join("wapm.toml")) { let manifest = match LocalPackage::read_toml(&path) {
Ok(o) => o,
Err(_) => continue,
};
let manifest = match wapm_toml::Manifest::parse(&s) {
Ok(o) => o, Ok(o) => o,
Err(_) => continue, Err(_) => continue,
}; };
@ -229,16 +242,11 @@ pub fn get_all_local_packages(#[cfg(test)] test_name: &str) -> Vec<LocalPackage>
} }
pub fn get_local_package( pub fn get_local_package(
#[cfg(test)] test_name: &str, wasmer_dir: &Path,
name: &str, name: &str,
version: Option<&str>, version: Option<&str>,
) -> Option<LocalPackage> { ) -> Option<LocalPackage> {
#[cfg(not(test))] get_all_local_packages(wasmer_dir)
let local_packages = get_all_local_packages();
#[cfg(test)]
let local_packages = get_all_local_packages(test_name);
local_packages
.iter() .iter()
.find(|p| { .find(|p| {
if p.name != name { if p.name != name {
@ -367,7 +375,7 @@ pub fn query_package_from_registry(
QueryPackageError::ErrorSendingQuery(format!("no package version for {name:?}")) QueryPackageError::ErrorSendingQuery(format!("no package version for {name:?}"))
})?; })?;
let manifest = toml::from_str::<wapm_toml::Manifest>(&v.manifest).map_err(|e| { let manifest = toml::from_str::<wasmer_toml::Manifest>(&v.manifest).map_err(|e| {
QueryPackageError::ErrorSendingQuery(format!("Invalid manifest for crate {name:?}: {e}")) QueryPackageError::ErrorSendingQuery(format!("Invalid manifest for crate {name:?}: {e}"))
})?; })?;
@ -392,43 +400,12 @@ pub fn query_package_from_registry(
}) })
} }
pub fn get_wasmer_root_dir(#[cfg(test)] test_name: &str) -> Option<PathBuf> { pub fn get_checkouts_dir(wasmer_dir: &Path) -> PathBuf {
#[cfg(test)] wasmer_dir.join("checkouts")
{
PartialWapmConfig::get_folder(test_name).ok()
}
#[cfg(not(test))]
{
PartialWapmConfig::get_folder().ok()
}
} }
pub fn get_checkouts_dir(#[cfg(test)] test_name: &str) -> Option<PathBuf> { pub fn get_webc_dir(wasmer_dir: &Path) -> PathBuf {
#[cfg(test)] wasmer_dir.join("webc")
let root_dir = get_wasmer_root_dir(test_name)?;
#[cfg(not(test))]
let root_dir = get_wasmer_root_dir()?;
Some(root_dir.join("checkouts"))
}
pub fn get_webc_dir(#[cfg(test)] test_name: &str) -> Option<PathBuf> {
#[cfg(test)]
let root_dir = get_wasmer_root_dir(test_name)?;
#[cfg(not(test))]
let root_dir = get_wasmer_root_dir()?;
Some(root_dir.join("webc"))
}
/// Returs the path to the directory where all packages on this computer are being stored
pub fn get_global_install_dir(
#[cfg(test)] test_name: &str,
registry_host: &str,
) -> Option<PathBuf> {
#[cfg(test)]
let root_dir = get_checkouts_dir(test_name)?;
#[cfg(not(test))]
let root_dir = get_checkouts_dir()?;
Some(root_dir.join(registry_host))
} }
/// Convenience function that will unpack .tar.gz files and .tar.bz /// Convenience function that will unpack .tar.gz files and .tar.bz
@ -538,7 +515,7 @@ where
/// Installs the .tar.gz if it doesn't yet exist, returns the /// Installs the .tar.gz if it doesn't yet exist, returns the
/// (package dir, entrypoint .wasm file path) /// (package dir, entrypoint .wasm file path)
pub fn install_package(#[cfg(test)] test_name: &str, url: &Url) -> Result<PathBuf, anyhow::Error> { pub fn install_package(wasmer_dir: &Path, url: &Url) -> Result<PathBuf, anyhow::Error> {
use fs_extra::dir::copy; use fs_extra::dir::copy;
let tempdir = tempdir::TempDir::new("download") let tempdir = tempdir::TempDir::new("download")
@ -563,21 +540,13 @@ pub fn install_package(#[cfg(test)] test_name: &str, url: &Url) -> Result<PathBu
) )
.with_context(|| anyhow::anyhow!("Could not unpack file downloaded from {url}"))?; .with_context(|| anyhow::anyhow!("Could not unpack file downloaded from {url}"))?;
// read {unpacked}/wapm.toml to get the name + version number // read {unpacked}/wasmer.toml to get the name + version number
let toml_path = unpacked_targz_path.join("wapm.toml"); let toml_parsed = LocalPackage::read_toml(&unpacked_targz_path)
let toml = std::fs::read_to_string(&toml_path) .map_err(|e| anyhow::anyhow!("error reading package name / version number: {e}"))?;
.map_err(|e| anyhow::anyhow!("error reading {}: {e}", toml_path.display()))?;
let toml_parsed = toml::from_str::<wapm_toml::Manifest>(&toml)
.map_err(|e| anyhow::anyhow!("error parsing {}: {e}", toml_path.display()))?;
let version = toml_parsed.package.version.to_string(); let version = toml_parsed.package.version.to_string();
#[cfg(test)] let checkouts_dir = crate::get_checkouts_dir(wasmer_dir);
let checkouts_dir = crate::get_checkouts_dir(test_name);
#[cfg(not(test))]
let checkouts_dir = crate::get_checkouts_dir();
let checkouts_dir = checkouts_dir.ok_or_else(|| anyhow::anyhow!("no checkouts dir"))?;
let installation_path = let installation_path =
checkouts_dir.join(format!("{}@{version}", Package::hash_url(url.as_ref()))); checkouts_dir.join(format!("{}@{version}", Package::hash_url(url.as_ref())));
@ -592,7 +561,7 @@ pub fn install_package(#[cfg(test)] test_name: &str, url: &Url) -> Result<PathBu
#[cfg(not(target_os = "wasi"))] #[cfg(not(target_os = "wasi"))]
let _ = filetime::set_file_mtime( let _ = filetime::set_file_mtime(
installation_path.join("wapm.toml"), LocalPackage::get_wasmer_toml_path(&installation_path)?,
filetime::FileTime::now(), filetime::FileTime::now(),
); );
@ -600,16 +569,14 @@ pub fn install_package(#[cfg(test)] test_name: &str, url: &Url) -> Result<PathBu
} }
pub fn whoami( pub fn whoami(
#[cfg(test)] test_name: &str, wasmer_dir: &Path,
registry: Option<&str>, registry: Option<&str>,
token: Option<&str>,
) -> Result<(String, String), anyhow::Error> { ) -> Result<(String, String), anyhow::Error> {
use crate::queries::{who_am_i_query, WhoAmIQuery}; use crate::queries::{who_am_i_query, WhoAmIQuery};
use graphql_client::GraphQLQuery; use graphql_client::GraphQLQuery;
#[cfg(test)] let config = WasmerConfig::from_file(wasmer_dir);
let config = PartialWapmConfig::from_file(test_name);
#[cfg(not(test))]
let config = PartialWapmConfig::from_file();
let config = config let config = config
.map_err(|e| anyhow::anyhow!("{e}")) .map_err(|e| anyhow::anyhow!("{e}"))
@ -620,9 +587,9 @@ pub fn whoami(
None => config.registry.get_current_registry(), None => config.registry.get_current_registry(),
}; };
let login_token = config let login_token = token
.registry .map(|s| s.to_string())
.get_login_token_for_registry(&registry) .or_else(|| config.registry.get_login_token_for_registry(&registry))
.ok_or_else(|| anyhow::anyhow!("not logged into registry {:?}", registry))?; .ok_or_else(|| anyhow::anyhow!("not logged into registry {:?}", registry))?;
let q = WhoAmIQuery::build_query(who_am_i_query::Variables {}); let q = WhoAmIQuery::build_query(who_am_i_query::Variables {});
@ -657,22 +624,11 @@ pub fn test_if_registry_present(registry: &str) -> Result<bool, String> {
Ok(true) Ok(true)
} }
pub fn get_all_available_registries(#[cfg(test)] test_name: &str) -> Result<Vec<String>, String> { pub fn get_all_available_registries(wasmer_dir: &Path) -> Result<Vec<String>, String> {
#[cfg(test)] let config = WasmerConfig::from_file(wasmer_dir)?;
let config = PartialWapmConfig::from_file(test_name)?;
#[cfg(not(test))]
let config = PartialWapmConfig::from_file()?;
let mut registries = Vec::new(); let mut registries = Vec::new();
match config.registry { for login in config.registry.tokens {
Registries::Single(s) => { registries.push(format_graphql(&login.registry));
registries.push(format_graphql(&s.url));
}
Registries::Multi(m) => {
for key in m.tokens.keys() {
registries.push(format_graphql(key));
}
}
} }
Ok(registries) Ok(registries)
} }
@ -684,7 +640,7 @@ pub struct RemoteWebcInfo {
} }
pub fn install_webc_package( pub fn install_webc_package(
#[cfg(test)] test_name: &str, wasmer_dir: &Path,
url: &Url, url: &Url,
checksum: &str, checksum: &str,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
@ -692,34 +648,18 @@ pub fn install_webc_package(
.enable_all() .enable_all()
.build() .build()
.unwrap() .unwrap()
.block_on(async { .block_on(async { install_webc_package_inner(wasmer_dir, url, checksum).await })
{
#[cfg(test)]
{
install_webc_package_inner(test_name, url, checksum).await
}
#[cfg(not(test))]
{
install_webc_package_inner(url, checksum).await
}
}
})
} }
async fn install_webc_package_inner( async fn install_webc_package_inner(
#[cfg(test)] test_name: &str, wasmer_dir: &Path,
url: &Url, url: &Url,
checksum: &str, checksum: &str,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
use futures_util::StreamExt; use futures_util::StreamExt;
#[cfg(test)] let path = get_webc_dir(wasmer_dir);
let path = get_webc_dir(test_name).ok_or_else(|| anyhow::anyhow!("no webc dir"))?;
#[cfg(not(test))]
let path = get_webc_dir().ok_or_else(|| anyhow::anyhow!("no webc dir"))?;
let _ = std::fs::create_dir_all(&path); let _ = std::fs::create_dir_all(&path);
let webc_path = path.join(checksum); let webc_path = path.join(checksum);
let mut file = std::fs::File::create(&webc_path) let mut file = std::fs::File::create(&webc_path)
@ -763,28 +703,12 @@ async fn install_webc_package_inner(
} }
/// Returns a list of all installed webc packages /// Returns a list of all installed webc packages
#[cfg(test)] pub fn get_all_installed_webc_packages(wasmer_dir: &Path) -> Vec<RemoteWebcInfo> {
pub fn get_all_installed_webc_packages(test_name: &str) -> Vec<RemoteWebcInfo> { get_all_installed_webc_packages_inner(wasmer_dir)
get_all_installed_webc_packages_inner(test_name)
} }
#[cfg(not(test))] fn get_all_installed_webc_packages_inner(wasmer_dir: &Path) -> Vec<RemoteWebcInfo> {
pub fn get_all_installed_webc_packages() -> Vec<RemoteWebcInfo> { let dir = get_webc_dir(wasmer_dir);
get_all_installed_webc_packages_inner("")
}
fn get_all_installed_webc_packages_inner(_test_name: &str) -> Vec<RemoteWebcInfo> {
#[cfg(test)]
let dir = match get_webc_dir(_test_name) {
Some(s) => s,
None => return Vec::new(),
};
#[cfg(not(test))]
let dir = match get_webc_dir() {
Some(s) => s,
None => return Vec::new(),
};
let read_dir = match std::fs::read_dir(dir) { let read_dir = match std::fs::read_dir(dir) {
Ok(s) => s, Ok(s) => s,
@ -984,8 +908,6 @@ fn get_bytes(
#[cfg(not(target_env = "musl"))] #[cfg(not(target_env = "musl"))]
#[test] #[test]
fn test_install_package() { fn test_install_package() {
const TEST_NAME: &str = "test_install_package";
println!("test install package..."); println!("test install package...");
let registry = "https://registry.wapm.io/graphql"; let registry = "https://registry.wapm.io/graphql";
if !test_if_registry_present(registry).unwrap_or(false) { if !test_if_registry_present(registry).unwrap_or(false) {
@ -1009,25 +931,25 @@ fn test_install_package() {
"https://registry-cdn.wapm.io/packages/wasmer/wabt/wabt-1.0.29.tar.gz".to_string() "https://registry-cdn.wapm.io/packages/wasmer/wabt/wabt-1.0.29.tar.gz".to_string()
); );
let path = install_package(TEST_NAME, &url::Url::parse(&wabt.url).unwrap()).unwrap(); let fake_wasmer_dir = tempdir::TempDir::new("tmp").unwrap();
let wasmer_dir = fake_wasmer_dir.path();
let path = install_package(wasmer_dir, &url::Url::parse(&wabt.url).unwrap()).unwrap();
println!("package installed: {path:?}"); println!("package installed: {path:?}");
assert_eq!( assert_eq!(
path, path,
get_checkouts_dir(TEST_NAME) get_checkouts_dir(wasmer_dir).join(&format!("{}@1.0.29", Package::hash_url(&wabt.url)))
.unwrap()
.join(&format!("{}@1.0.29", Package::hash_url(&wabt.url)))
); );
let all_installed_packages = get_all_local_packages(TEST_NAME); let all_installed_packages = get_all_local_packages(wasmer_dir);
let is_installed = all_installed_packages let is_installed = all_installed_packages
.iter() .iter()
.any(|p| p.name == "wasmer/wabt" && p.version == "1.0.29"); .any(|p| p.name == "wasmer/wabt" && p.version == "1.0.29");
if !is_installed { if !is_installed {
let panic_str = get_all_local_packages(TEST_NAME) let panic_str = get_all_local_packages(wasmer_dir)
.iter() .iter()
.map(|p| format!("{} {} {}", p.registry, p.name, p.version)) .map(|p| format!("{} {} {}", p.registry, p.name, p.version))
.collect::<Vec<_>>() .collect::<Vec<_>>()

View File

@ -1,33 +1,25 @@
use crate::config::{format_graphql, UpdateRegistry}; use crate::config::{format_graphql, UpdateRegistry};
use crate::PartialWapmConfig; use crate::WasmerConfig;
use std::path::Path;
/// Login to a registry and save the token associated with it. /// Login to a registry and save the token associated with it.
/// ///
/// Also sets the registry as the currently active registry to provide a better UX. /// Also sets the registry as the currently active registry to provide a better UX.
pub fn login_and_save_token( pub fn login_and_save_token(
#[cfg(test)] test_name: &str, wasmer_dir: &Path,
registry: &str, registry: &str,
token: &str, token: &str,
) -> Result<Option<String>, anyhow::Error> { ) -> Result<Option<String>, anyhow::Error> {
let registry = format_graphql(registry); let registry = format_graphql(registry);
#[cfg(test)] let mut config = WasmerConfig::from_file(wasmer_dir)
let mut config = PartialWapmConfig::from_file(test_name)
.map_err(|e| anyhow::anyhow!("config from file: {e}"))?; .map_err(|e| anyhow::anyhow!("config from file: {e}"))?;
#[cfg(not(test))]
let mut config =
PartialWapmConfig::from_file().map_err(|e| anyhow::anyhow!("config from file: {e}"))?;
config.registry.set_current_registry(&registry); config.registry.set_current_registry(&registry);
config.registry.set_login_token_for_registry( config.registry.set_login_token_for_registry(
&config.registry.get_current_registry(), &config.registry.get_current_registry(),
token, token,
UpdateRegistry::Update, UpdateRegistry::Update,
); );
#[cfg(test)] let path = WasmerConfig::get_file_location(wasmer_dir);
let path = PartialWapmConfig::get_file_location(test_name)
.map_err(|e| anyhow::anyhow!("get file location: {e}"))?;
#[cfg(not(test))]
let path = PartialWapmConfig::get_file_location()
.map_err(|e| anyhow::anyhow!("get file location: {e}"))?;
config.save(&path)?; config.save(&path)?;
crate::utils::get_username_registry_token(&registry, token) crate::utils::get_username_registry_token(&registry, token)
} }

View File

@ -1,8 +1,16 @@
use crate::PartialWapmConfig; use crate::WasmerConfig;
use std::path::PathBuf; use regex::Regex;
use std::path::{Path, PathBuf};
use std::{fmt, str::FromStr}; use std::{fmt, str::FromStr};
use url::Url; use url::Url;
const REGEX_PACKAGE_WITH_VERSION: &str =
r#"^([a-zA-Z0-9\-_]+)/([a-zA-Z0-9\-_]+)(@([a-zA-Z0-9\.\-_]+*))?$"#;
lazy_static::lazy_static! {
static ref PACKAGE_WITH_VERSION: Regex = regex::Regex::new(REGEX_PACKAGE_WITH_VERSION).unwrap();
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Package { pub struct Package {
pub namespace: String, pub namespace: String,
@ -18,16 +26,11 @@ impl fmt::Display for Package {
impl Package { impl Package {
/// Checks whether the package is already installed, if yes, returns the path to the root dir /// Checks whether the package is already installed, if yes, returns the path to the root dir
pub fn already_installed(&self, #[cfg(test)] test_name: &str) -> Option<PathBuf> { pub fn already_installed(&self, wasmer_dir: &Path) -> Option<PathBuf> {
#[cfg(not(test))] let checkouts_dir = crate::get_checkouts_dir(wasmer_dir);
let checkouts_dir = crate::get_checkouts_dir()?; let config = WasmerConfig::from_file(wasmer_dir).ok()?;
#[cfg(test)] let current_registry = config.registry.get_current_registry();
let checkouts_dir = crate::get_checkouts_dir(test_name)?; let hash = self.get_hash(&current_registry);
#[cfg(not(test))]
let hash = self.get_hash();
#[cfg(test)]
let hash = self.get_hash(test_name);
let found = std::fs::read_dir(&checkouts_dir) let found = std::fs::read_dir(&checkouts_dir)
.ok()? .ok()?
@ -41,11 +44,8 @@ impl Package {
/// Checks if the URL is already installed, note that `{url}@{version}` /// Checks if the URL is already installed, note that `{url}@{version}`
/// and `{url}` are treated the same /// and `{url}` are treated the same
pub fn is_url_already_installed(url: &Url, #[cfg(test)] test_name: &str) -> Option<PathBuf> { pub fn is_url_already_installed(url: &Url, wasmer_dir: &Path) -> Option<PathBuf> {
#[cfg(not(test))] let checkouts_dir = crate::get_checkouts_dir(wasmer_dir);
let checkouts_dir = crate::get_checkouts_dir()?;
#[cfg(test)]
let checkouts_dir = crate::get_checkouts_dir(test_name)?;
let url_string = url.to_string(); let url_string = url.to_string();
let (url, version) = match url_string.split('@').collect::<Vec<_>>()[..] { let (url, version) = match url_string.split('@').collect::<Vec<_>>()[..] {
@ -76,22 +76,13 @@ impl Package {
/// Returns the hash of the package URL without the version /// Returns the hash of the package URL without the version
/// (because the version is encoded as @version and isn't part of the hash itself) /// (because the version is encoded as @version and isn't part of the hash itself)
pub fn get_hash(&self, #[cfg(test)] test_name: &str) -> String { pub fn get_hash(&self, registry: &str) -> String {
#[cfg(test)] let url = self.get_url_without_version(registry);
let url = self.get_url_without_version(test_name);
#[cfg(not(test))]
let url = self.get_url_without_version();
Self::hash_url(&url.unwrap_or_default()) Self::hash_url(&url.unwrap_or_default())
} }
fn get_url_without_version( fn get_url_without_version(&self, registry: &str) -> Result<String, anyhow::Error> {
&self, let url = self.url(registry);
#[cfg(test)] test_name: &str,
) -> Result<String, anyhow::Error> {
#[cfg(test)]
let url = self.url(test_name);
#[cfg(not(test))]
let url = self.url();
Ok(format!( Ok(format!(
"{}/{}/{}", "{}/{}/{}",
url?.origin().ascii_serialization(), url?.origin().ascii_serialization(),
@ -116,16 +107,9 @@ impl Package {
} }
/// Returns the full URL including the version for this package /// Returns the full URL including the version for this package
pub fn url(&self, #[cfg(test)] test_name: &str) -> Result<Url, anyhow::Error> { pub fn url(&self, registry: &str) -> Result<Url, anyhow::Error> {
#[cfg(test)]
let config = PartialWapmConfig::from_file(test_name)
.map_err(|e| anyhow::anyhow!("could not read wapm config: {e}"))?;
#[cfg(not(test))]
let config = PartialWapmConfig::from_file()
.map_err(|e| anyhow::anyhow!("could not read wapm config: {e}"))?;
let registry = config.registry.get_current_registry();
let registry_tld = tldextract::TldExtractor::new(tldextract::TldOption::default()) let registry_tld = tldextract::TldExtractor::new(tldextract::TldOption::default())
.extract(&registry) .extract(registry)
.map_err(|e| anyhow::anyhow!("Invalid registry: {}: {e}", registry))?; .map_err(|e| anyhow::anyhow!("Invalid registry: {}: {e}", registry))?;
let registry_tld = format!( let registry_tld = format!(
@ -148,18 +132,11 @@ impl Package {
/// Returns the path to the installation directory. /// Returns the path to the installation directory.
/// Does not check whether the installation directory already exists. /// Does not check whether the installation directory already exists.
pub fn get_path(&self, #[cfg(test)] test_name: &str) -> Result<PathBuf, anyhow::Error> { pub fn get_path(&self, wasmer_dir: &Path) -> Result<PathBuf, anyhow::Error> {
#[cfg(test)] let checkouts_dir = crate::get_checkouts_dir(wasmer_dir);
let checkouts_dir = crate::get_checkouts_dir(test_name); let config = WasmerConfig::from_file(wasmer_dir)
#[cfg(not(test))] .map_err(|e| anyhow::anyhow!("could not load config {e}"))?;
let checkouts_dir = crate::get_checkouts_dir(); let hash = self.get_hash(&config.registry.get_current_registry());
let checkouts_dir = checkouts_dir.ok_or_else(|| anyhow::anyhow!("no checkouts dir"))?;
#[cfg(not(test))]
let hash = self.get_hash();
#[cfg(test)]
let hash = self.get_hash(test_name);
match self.version.as_ref() { match self.version.as_ref() {
Some(v) => Ok(checkouts_dir.join(format!("{}@{}", hash, v))), Some(v) => Ok(checkouts_dir.join(format!("{}@{}", hash, v))),
@ -172,11 +149,7 @@ impl FromStr for Package {
type Err = anyhow::Error; type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let regex = let captures = PACKAGE_WITH_VERSION
regex::Regex::new(r#"^([a-zA-Z0-9\-_]+)/([a-zA-Z0-9\-_]+)(@([a-zA-Z0-9\.\-_]+*))?$"#)
.unwrap();
let captures = regex
.captures(s.trim()) .captures(s.trim())
.map(|c| { .map(|c| {
c.iter() c.iter()

241
lib/registry/src/publish.rs Normal file
View File

@ -0,0 +1,241 @@
use crate::graphql::{execute_query_modifier_inner, get_signed_url, GetSignedUrl};
use crate::graphql::{publish_package_mutation_chunked, PublishPackageMutationChunked};
use crate::{format_graphql, WasmerConfig};
use console::{style, Emoji};
use graphql_client::GraphQLQuery;
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
use std::collections::BTreeMap;
use std::fmt::Write;
use std::io::BufRead;
use std::path::PathBuf;
static UPLOAD: Emoji<'_, '_> = Emoji("⬆️ ", "");
static PACKAGE: Emoji<'_, '_> = Emoji("📦 ", "");
#[derive(Debug, Clone)]
pub enum SignArchiveResult {
Ok {
public_key_id: String,
signature: String,
},
NoKeyRegistered,
}
#[allow(clippy::too_many_arguments)]
pub fn try_chunked_uploading(
registry: Option<String>,
token: Option<String>,
package: &wasmer_toml::Package,
manifest_string: &String,
license_file: &Option<String>,
readme: &Option<String>,
archive_name: &String,
archive_path: &PathBuf,
maybe_signature_data: &SignArchiveResult,
archived_data_size: u64,
quiet: bool,
) -> Result<(), anyhow::Error> {
let registry = match registry.as_ref() {
Some(s) => format_graphql(s),
None => {
let wasmer_dir = WasmerConfig::get_wasmer_dir().map_err(|e| anyhow::anyhow!("{e}"))?;
let config = WasmerConfig::from_file(&wasmer_dir);
config
.map_err(|e| anyhow::anyhow!("{e}"))?
.registry
.get_current_registry()
}
};
let token = match token.as_ref() {
Some(s) => s.to_string(),
None => {
let wasmer_dir = WasmerConfig::get_wasmer_dir().map_err(|e| anyhow::anyhow!("{e}"))?;
let config = WasmerConfig::from_file(&wasmer_dir);
config
.map_err(|e| anyhow::anyhow!("{e}"))?
.registry
.get_login_token_for_registry(&registry)
.ok_or_else(|| {
anyhow::anyhow!("cannot publish package: not logged into registry {registry:?}")
})?
}
};
let maybe_signature_data = match maybe_signature_data {
SignArchiveResult::Ok {
public_key_id,
signature,
} => {
log::info!(
"Package successfully signed with public key: \"{}\"!",
&public_key_id
);
Some(publish_package_mutation_chunked::InputSignature {
public_key_key_id: public_key_id.to_string(),
data: signature.to_string(),
})
}
SignArchiveResult::NoKeyRegistered => {
// TODO: uncomment this when we actually want users to start using it
//warn!("Publishing package without a verifying signature. Consider registering a key pair with wapm");
None
}
};
if !quiet {
println!("{} {} Uploading...", style("[1/2]").bold().dim(), UPLOAD);
}
let get_google_signed_url = GetSignedUrl::build_query(get_signed_url::Variables {
name: package.name.to_string(),
version: package.version.to_string(),
expires_after_seconds: Some(60 * 30),
});
let _response: get_signed_url::ResponseData =
execute_query_modifier_inner(&registry, &token, &get_google_signed_url, None, |f| f)?;
let url = _response.url.ok_or_else(|| {
anyhow::anyhow!(
"could not get signed url for package {}@{}",
package.name,
package.version
)
})?;
let signed_url = url.url;
let url = url::Url::parse(&signed_url).unwrap();
let client = reqwest::blocking::Client::builder()
.default_headers(reqwest::header::HeaderMap::default())
.build()
.unwrap();
let res = client
.post(url)
.header(reqwest::header::CONTENT_LENGTH, "0")
.header(reqwest::header::CONTENT_TYPE, "application/octet-stream")
.header("x-goog-resumable", "start");
let result = res.send().unwrap();
if result.status() != reqwest::StatusCode::from_u16(201).unwrap() {
return Err(anyhow::anyhow!(
"Uploading package failed: got HTTP {:?} when uploading",
result.status()
));
}
let headers = result
.headers()
.into_iter()
.filter_map(|(k, v)| {
let k = k.to_string();
let v = v.to_str().ok()?.to_string();
Some((k.to_lowercase(), v))
})
.collect::<BTreeMap<_, _>>();
let session_uri = headers.get("location").unwrap().clone();
let total = archived_data_size;
// archive_path
let mut file = std::fs::OpenOptions::new()
.read(true)
.open(archive_path)
.map_err(|e| anyhow::anyhow!("cannot open archive {}: {e}", archive_path.display()))?;
let pb = ProgressBar::new(archived_data_size);
pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})")
.unwrap()
.with_key("eta", |state: &ProgressState, w: &mut dyn Write| {
write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
})
.progress_chars("#>-"));
let chunk_size = 1_048_576; // 1MB - 315s / 100MB
let mut file_pointer = 0;
let mut reader = std::io::BufReader::with_capacity(chunk_size, &mut file);
let client = reqwest::blocking::Client::builder()
.default_headers(reqwest::header::HeaderMap::default())
.build()
.unwrap();
while let Some(chunk) = reader.fill_buf().ok().map(|s| s.to_vec()) {
let n = chunk.len();
if chunk.is_empty() {
break;
}
let start = file_pointer;
let end = file_pointer + chunk.len().saturating_sub(1);
let content_range = format!("bytes {start}-{end}/{total}");
let res = client
.put(&session_uri)
.header(reqwest::header::CONTENT_TYPE, "application/octet-stream")
.header(reqwest::header::CONTENT_LENGTH, format!("{}", chunk.len()))
.header("Content-Range".to_string(), content_range)
.body(chunk.to_vec());
pb.set_position(file_pointer as u64);
res.send()
.map(|response| response.error_for_status())
.map_err(|e| {
anyhow::anyhow!(
"cannot send request to {session_uri} (chunk {}..{}): {e}",
file_pointer,
file_pointer + chunk_size
)
})??;
if n < chunk_size {
break;
}
reader.consume(n);
file_pointer += n;
}
pb.finish_and_clear();
if !quiet {
println!("{} {}Publishing...", style("[2/2]").bold().dim(), PACKAGE);
}
let q =
PublishPackageMutationChunked::build_query(publish_package_mutation_chunked::Variables {
name: package.name.to_string(),
version: package.version.to_string(),
description: package.description.clone(),
manifest: manifest_string.to_string(),
license: package.license.clone(),
license_file: license_file.to_owned(),
readme: readme.to_owned(),
repository: package.repository.clone(),
homepage: package.homepage.clone(),
file_name: Some(archive_name.to_string()),
signature: maybe_signature_data,
signed_url: Some(signed_url),
});
let _response: publish_package_mutation_chunked::ResponseData =
crate::graphql::execute_query(&registry, &token, &q)?;
println!(
"Successfully published package `{}@{}`",
package.name, package.version
);
Ok(())
}

View File

@ -1,4 +1,4 @@
use crate::{graphql::execute_query, PartialWapmConfig}; use crate::graphql::execute_query;
use graphql_client::GraphQLQuery; use graphql_client::GraphQLQuery;
#[derive(GraphQLQuery)] #[derive(GraphQLQuery)]
@ -9,12 +9,7 @@ use graphql_client::GraphQLQuery;
)] )]
struct WhoAmIQuery; struct WhoAmIQuery;
pub fn get_username(#[cfg(test)] test_name: &str) -> anyhow::Result<Option<String>> { pub fn get_username(registry: &str) -> anyhow::Result<Option<String>> {
#[cfg(test)]
let config = PartialWapmConfig::from_file(test_name).map_err(|e| anyhow::anyhow!("{e}"))?;
#[cfg(not(test))]
let config = PartialWapmConfig::from_file().map_err(|e| anyhow::anyhow!("{e}"))?;
let registry = &config.registry.get_current_registry();
let q = WhoAmIQuery::build_query(who_am_i_query::Variables {}); let q = WhoAmIQuery::build_query(who_am_i_query::Variables {});
let response: who_am_i_query::ResponseData = execute_query(registry, "", &q)?; let response: who_am_i_query::ResponseData = execute_query(registry, "", &q)?;
Ok(response.viewer.map(|viewer| viewer.username)) Ok(response.viewer.map(|viewer| viewer.username))

View File

@ -15,7 +15,7 @@ thiserror = "1"
tracing = { version = "0.1" } tracing = { version = "0.1" }
typetag = { version = "0.1", optional = true } typetag = { version = "0.1", optional = true }
serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true }
webc = { version = "0.4.1", optional = true } webc = { version = "4.0.0", optional = true }
slab = { version = "0.4" } slab = { version = "0.4" }
derivative = "2.2.0" derivative = "2.2.0"
anyhow = { version = "1.0.66", optional = true } anyhow = { version = "1.0.66", optional = true }

View File

@ -7,12 +7,8 @@
use more_asserts::assert_le; use more_asserts::assert_le;
use more_asserts::assert_lt; use more_asserts::assert_lt;
use std::io; use std::io;
use std::io::Read;
use std::io::Write;
use std::ptr; use std::ptr;
use std::slice; use std::slice;
#[cfg(feature = "tracing")]
use tracing::trace;
/// Round `size` up to the nearest multiple of `page_size`. /// Round `size` up to the nearest multiple of `page_size`.
fn round_up_to_page_size(size: usize, page_size: usize) -> usize { fn round_up_to_page_size(size: usize, page_size: usize) -> usize {
@ -28,35 +24,8 @@ pub struct Mmap {
// `unsafe impl`. This type is sendable across threads and shareable since // `unsafe impl`. This type is sendable across threads and shareable since
// the coordination all happens at the OS layer. // the coordination all happens at the OS layer.
ptr: usize, ptr: usize,
len: usize, total_size: usize,
// Backing file that will be closed when the memory mapping goes out of scope accessible_size: usize,
fd: FdGuard,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FdGuard(pub i32);
impl Default for FdGuard {
fn default() -> Self {
Self(-1)
}
}
impl Clone for FdGuard {
fn clone(&self) -> Self {
unsafe { Self(libc::dup(self.0)) }
}
}
impl Drop for FdGuard {
fn drop(&mut self) {
if self.0 >= 0 {
unsafe {
libc::close(self.0);
}
self.0 = -1;
}
}
} }
impl Mmap { impl Mmap {
@ -68,8 +37,8 @@ impl Mmap {
let empty = Vec::<u8>::new(); let empty = Vec::<u8>::new();
Self { Self {
ptr: empty.as_ptr() as usize, ptr: empty.as_ptr() as usize,
len: 0, total_size: 0,
fd: FdGuard::default(), accessible_size: 0,
} }
} }
@ -99,28 +68,6 @@ impl Mmap {
return Ok(Self::new()); return Ok(Self::new());
} }
// Open a temporary file (which is used for swapping)
let fd = unsafe {
let file = libc::tmpfile();
if file.is_null() {
return Err(format!(
"failed to create temporary file - {}",
io::Error::last_os_error()
));
}
FdGuard(libc::fileno(file))
};
// First we initialize it with zeros
unsafe {
if libc::ftruncate(fd.0, mapping_size as libc::off_t) < 0 {
return Err("could not truncate tmpfile".to_string());
}
}
// Compute the flags
let flags = libc::MAP_FILE | libc::MAP_SHARED;
Ok(if accessible_size == mapping_size { Ok(if accessible_size == mapping_size {
// Allocate a single read-write region at once. // Allocate a single read-write region at once.
let ptr = unsafe { let ptr = unsafe {
@ -128,8 +75,8 @@ impl Mmap {
ptr::null_mut(), ptr::null_mut(),
mapping_size, mapping_size,
libc::PROT_READ | libc::PROT_WRITE, libc::PROT_READ | libc::PROT_WRITE,
flags, libc::MAP_PRIVATE | libc::MAP_ANON,
fd.0, -1,
0, 0,
) )
}; };
@ -139,8 +86,8 @@ impl Mmap {
Self { Self {
ptr: ptr as usize, ptr: ptr as usize,
len: mapping_size, total_size: mapping_size,
fd, accessible_size,
} }
} else { } else {
// Reserve the mapping size. // Reserve the mapping size.
@ -149,8 +96,8 @@ impl Mmap {
ptr::null_mut(), ptr::null_mut(),
mapping_size, mapping_size,
libc::PROT_NONE, libc::PROT_NONE,
flags, libc::MAP_PRIVATE | libc::MAP_ANON,
fd.0, -1,
0, 0,
) )
}; };
@ -160,8 +107,8 @@ impl Mmap {
let mut result = Self { let mut result = Self {
ptr: ptr as usize, ptr: ptr as usize,
len: mapping_size, total_size: mapping_size,
fd, accessible_size,
}; };
if accessible_size != 0 { if accessible_size != 0 {
@ -194,7 +141,8 @@ impl Mmap {
if mapping_size == 0 { if mapping_size == 0 {
return Ok(Self::new()); return Ok(Self::new());
} }
if accessible_size == mapping_size {
Ok(if accessible_size == mapping_size {
// Allocate a single read-write region at once. // Allocate a single read-write region at once.
let ptr = unsafe { let ptr = unsafe {
VirtualAlloc( VirtualAlloc(
@ -208,11 +156,10 @@ impl Mmap {
return Err(io::Error::last_os_error().to_string()); return Err(io::Error::last_os_error().to_string());
} }
Ok(Self { Self {
ptr: ptr as usize, ptr: ptr as usize,
len: mapping_size, len: mapping_size,
fd: FdGuard::default(), }
})
} else { } else {
// Reserve the mapping size. // Reserve the mapping size.
let ptr = let ptr =
@ -224,7 +171,6 @@ impl Mmap {
let mut result = Self { let mut result = Self {
ptr: ptr as usize, ptr: ptr as usize,
len: mapping_size, len: mapping_size,
fd: FdGuard::default(),
}; };
if accessible_size != 0 { if accessible_size != 0 {
@ -232,8 +178,8 @@ impl Mmap {
result.make_accessible(0, accessible_size)?; result.make_accessible(0, accessible_size)?;
} }
Ok(result) result
} })
} }
/// Make the memory starting at `start` and extending for `len` bytes accessible. /// Make the memory starting at `start` and extending for `len` bytes accessible.
@ -244,8 +190,8 @@ impl Mmap {
let page_size = region::page::size(); let page_size = region::page::size();
assert_eq!(start & (page_size - 1), 0); assert_eq!(start & (page_size - 1), 0);
assert_eq!(len & (page_size - 1), 0); assert_eq!(len & (page_size - 1), 0);
assert_lt!(len, self.len); assert_lt!(len, self.total_size);
assert_lt!(start, self.len - len); assert_lt!(start, self.total_size - len);
// Commit the accessible size. // Commit the accessible size.
let ptr = self.ptr as *const u8; let ptr = self.ptr as *const u8;
@ -287,12 +233,22 @@ impl Mmap {
/// Return the allocated memory as a slice of u8. /// Return the allocated memory as a slice of u8.
pub fn as_slice(&self) -> &[u8] { pub fn as_slice(&self) -> &[u8] {
unsafe { slice::from_raw_parts(self.ptr as *const u8, self.len) } unsafe { slice::from_raw_parts(self.ptr as *const u8, self.total_size) }
}
/// Return the allocated memory as a slice of u8.
pub fn as_slice_accessible(&self) -> &[u8] {
unsafe { slice::from_raw_parts(self.ptr as *const u8, self.accessible_size) }
} }
/// Return the allocated memory as a mutable slice of u8. /// Return the allocated memory as a mutable slice of u8.
pub fn as_mut_slice(&mut self) -> &mut [u8] { pub fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { slice::from_raw_parts_mut(self.ptr as *mut u8, self.len) } unsafe { slice::from_raw_parts_mut(self.ptr as *mut u8, self.total_size) }
}
/// Return the allocated memory as a mutable slice of u8.
pub fn as_mut_slice_accessible(&mut self) -> &mut [u8] {
unsafe { slice::from_raw_parts_mut(self.ptr as *mut u8, self.accessible_size) }
} }
/// Return the allocated memory as a pointer to u8. /// Return the allocated memory as a pointer to u8.
@ -307,7 +263,7 @@ impl Mmap {
/// Return the length of the allocated memory. /// Return the length of the allocated memory.
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.len self.total_size
} }
/// Return whether any memory has been allocated. /// Return whether any memory has been allocated.
@ -315,117 +271,20 @@ impl Mmap {
self.len() == 0 self.len() == 0
} }
/// Copies the memory to a new swap file (using copy-on-write if available) /// Duplicate in a new memory mapping.
#[cfg(not(target_os = "windows"))] pub fn duplicate(&mut self, _size_hint: Option<usize>) -> Result<Self, String> {
pub fn duplicate(&mut self, hint_used: Option<usize>) -> Result<Self, String> { let mut new = Self::accessible_reserved(self.accessible_size, self.total_size)?;
// Empty memory is an edge case new.as_mut_slice_accessible()
.copy_from_slice(self.as_slice_accessible());
use std::os::unix::prelude::FromRawFd; Ok(new)
if self.len == 0 {
return Ok(Self::new());
}
// First we sync all the data to the backing file
unsafe {
libc::fsync(self.fd.0);
}
// Open a new temporary file (which is used for swapping for the forked memory)
let fd = unsafe {
let file = libc::tmpfile();
if file.is_null() {
return Err(format!(
"failed to create temporary file - {}",
io::Error::last_os_error()
));
}
FdGuard(libc::fileno(file))
};
// Attempt to do a shallow copy (needs a backing file system that supports it)
unsafe {
if libc::ioctl(fd.0, 0x94, 9, self.fd.0) != 0
// FICLONE
{
#[cfg(feature = "tracing")]
trace!("memory copy started");
// Determine host much to copy
let len = match hint_used {
Some(a) => a,
None => self.len,
};
// The shallow copy failed so we have to do it the hard way
let mut source = std::fs::File::from_raw_fd(self.fd.0);
let mut out = std::fs::File::from_raw_fd(fd.0);
copy_file_range(&mut source, 0, &mut out, 0, len)
.map_err(|err| format!("Could not copy memory: {err}"))?;
#[cfg(feature = "tracing")]
trace!("memory copy finished (size={})", len);
}
}
// Compute the flags
let flags = libc::MAP_FILE | libc::MAP_SHARED;
// Allocate a single read-write region at once.
let ptr = unsafe {
libc::mmap(
ptr::null_mut(),
self.len,
libc::PROT_READ | libc::PROT_WRITE,
flags,
fd.0,
0,
)
};
if ptr as isize == -1_isize {
return Err(io::Error::last_os_error().to_string());
}
Ok(Self {
ptr: ptr as usize,
len: self.len,
fd,
})
}
/// Copies the memory to a new swap file (using copy-on-write if available)
#[cfg(target_os = "windows")]
pub fn duplicate(&mut self, hint_used: Option<usize>) -> Result<Self, String> {
// Create a new memory which we will copy to
let new_mmap = Self::with_at_least(self.len)?;
#[cfg(feature = "tracing")]
trace!("memory copy started");
// Determine host much to copy
let len = match hint_used {
Some(a) => a,
None => self.len,
};
// Copy the data to the new memory
let dst = new_mmap.ptr as *mut u8;
let src = self.ptr as *const u8;
unsafe {
std::ptr::copy_nonoverlapping(src, dst, len);
}
#[cfg(feature = "tracing")]
trace!("memory copy finished (size={})", len);
Ok(new_mmap)
} }
} }
impl Drop for Mmap { impl Drop for Mmap {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
fn drop(&mut self) { fn drop(&mut self) {
if self.len != 0 { if self.total_size != 0 {
let r = unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.len) }; let r = unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.total_size) };
assert_eq!(r, 0, "munmap failed: {}", io::Error::last_os_error()); assert_eq!(r, 0, "munmap failed: {}", io::Error::last_os_error());
} }
} }
@ -447,54 +306,6 @@ fn _assert() {
_assert_send_sync::<Mmap>(); _assert_send_sync::<Mmap>();
} }
/// Copy a range of a file to another file.
// We could also use libc::copy_file_range on some systems, but it's
// hard to do this because it is not available on many libc implementations.
// (not on Mac OS, musl, ...)
#[cfg(target_family = "unix")]
fn copy_file_range(
source: &mut std::fs::File,
source_offset: u64,
out: &mut std::fs::File,
out_offset: u64,
len: usize,
) -> Result<(), std::io::Error> {
use std::io::{Seek, SeekFrom};
let source_original_pos = source.stream_position()?;
source.seek(SeekFrom::Start(source_offset))?;
// TODO: don't cast with as
let out_original_pos = out.stream_position()?;
out.seek(SeekFrom::Start(out_offset))?;
// TODO: don't do this horrible "triple buffering" below".
// let mut reader = std::io::BufReader::new(source);
// TODO: larger buffer?
let mut buffer = vec![0u8; 4096];
let mut to_read = len;
while to_read > 0 {
let chunk_size = std::cmp::min(to_read, buffer.len());
let read = source.read(&mut buffer[0..chunk_size])?;
out.write_all(&buffer[0..read])?;
to_read -= read;
}
// Need to read the last chunk.
out.flush()?;
// Restore files to original position.
source.seek(SeekFrom::Start(source_original_pos))?;
out.flush()?;
out.sync_data()?;
out.seek(SeekFrom::Start(out_original_pos))?;
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -506,59 +317,4 @@ mod tests {
assert_eq!(round_up_to_page_size(4096, 4096), 4096); assert_eq!(round_up_to_page_size(4096, 4096), 4096);
assert_eq!(round_up_to_page_size(4097, 4096), 8192); assert_eq!(round_up_to_page_size(4097, 4096), 8192);
} }
#[cfg(target_family = "unix")]
#[test]
fn test_copy_file_range() -> Result<(), std::io::Error> {
// I know tempfile:: exists, but this doesn't bring in an extra
// dependency.
use std::{fs::OpenOptions, io::Seek};
let dir = std::env::temp_dir().join("wasmer/copy_file_range");
if dir.is_dir() {
std::fs::remove_dir_all(&dir).unwrap()
}
std::fs::create_dir_all(&dir).unwrap();
let pa = dir.join("a");
let pb = dir.join("b");
let data: Vec<u8> = (0..100).collect();
let mut a = OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&pa)
.unwrap();
a.write_all(&data).unwrap();
let datb: Vec<u8> = (100..200).collect();
let mut b = OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&pb)
.unwrap();
b.write_all(&datb).unwrap();
a.seek(io::SeekFrom::Start(30)).unwrap();
b.seek(io::SeekFrom::Start(99)).unwrap();
copy_file_range(&mut a, 10, &mut b, 40, 15).unwrap();
assert_eq!(a.stream_position().unwrap(), 30);
assert_eq!(b.stream_position().unwrap(), 99);
b.seek(io::SeekFrom::Start(0)).unwrap();
let mut out = Vec::new();
let len = b.read_to_end(&mut out).unwrap();
assert_eq!(len, 100);
assert_eq!(out[0..40], datb[0..40]);
assert_eq!(out[40..55], data[10..25]);
assert_eq!(out[55..100], datb[55..100]);
// TODO: needs more variant tests, but this is enough for now.
Ok(())
}
} }

View File

@ -30,7 +30,7 @@ bincode = { version = "1.3" }
chrono = { version = "^0.4", default-features = false, features = [ "wasmbind", "std", "clock" ], optional = true } chrono = { version = "^0.4", default-features = false, features = [ "wasmbind", "std", "clock" ], optional = true }
derivative = { version = "^2" } derivative = { version = "^2" }
bytes = "1" bytes = "1"
webc = { version = "0.4.1", default-features = false, features = ["std", "mmap"] } webc = { version = "4.0.0", optional = true, default-features = false, features = ["std", "mmap"] }
serde_cbor = { version = "0.11.2", optional = true } serde_cbor = { version = "0.11.2", optional = true }
anyhow = { version = "1.0.66" } anyhow = { version = "1.0.66" }
lazy_static = "1.4" lazy_static = "1.4"

View File

@ -0,0 +1,24 @@
[package]
name = "wasmer-wasm-interface"
version = "3.1.0"
authors = ["The Wasmer Engineering Team <engineering@wasmer.io>"]
edition = "2018"
repository = "https://github.com/wasmerio/wapm-cli"
description = "WASM Interface definition and parser"
readme = "README.md"
license = "MIT"
[dependencies]
bincode = { version = "1", optional = true }
either = "1.5"
nom = "5"
serde = { version = "1", features = ["derive"] }
wasmparser = { version = "0.51.4", optional = true }
[dev-dependencies]
wat = "1.0"
[features]
validation = ["wasmparser"]
binary_encode = ["bincode"]
default = ["validation"]

View File

@ -0,0 +1,88 @@
# Wasm Interface
This is an experimental crate for validating the imports and exports of a WebAssembly module.
For the time being, Wasm Interface provides:
- a convenient text format for specifying the requirements of Wasm modules
- a convenient way to compose interfaces safely (it ensures no conflicts (duplicates are allowed but they must agree))
- validation that the modules meet the requirements
## Syntax example
Here's the interface for the current version of [WASI](https://github.com/WebAssembly/WASI):
```lisp
(interface "wasi_unstable"
;; Here's a bunch of function imports!
(func (import "wasi_unstable" "args_get") (param i32 i32) (result i32))
(func (import "wasi_unstable" "args_sizes_get") (param i32 i32) (result i32))
(func (import "wasi_unstable" "clock_res_get") (param i32 i32) (result i32))
(func (import "wasi_unstable" "clock_time_get") (param i32 i32 i32) (result i32))
(func (import "wasi_unstable" "environ_get") (param i32 i32) (result i32))
(func (import "wasi_unstable" "environ_sizes_get") (param i32 i32) (result i32))
(func (import "wasi_unstable" "fd_advise") (param i32 i64 i64 i32) (result i32))
(func (import "wasi_unstable" "fd_allocate") (param i32 i64 i64) (result i32))
(func (import "wasi_unstable" "fd_close") (param i32) (result i32))
(func (import "wasi_unstable" "fd_datasync") (param i32) (result i32))
(func (import "wasi_unstable" "fd_fdstat_get") (param i32 i32) (result i32))
(func (import "wasi_unstable" "fd_fdstat_set_flags") (param i32 i32) (result i32))
(func (import "wasi_unstable" "fd_fdstat_set_rights") (param i32 i64 i64) (result i32))
(func (import "wasi_unstable" "fd_filestat_get") (param i32 i32) (result i32))
(func (import "wasi_unstable" "fd_filestat_set_size") (param i32 i64) (result i32))
(func (import "wasi_unstable" "fd_filestat_set_times") (param i32 i64 i64 i32) (result i32))
(func (import "wasi_unstable" "fd_pread") (param i32 i32 i32 i64 i32) (result i32))
(func (import "wasi_unstable" "fd_prestat_get") (param i32 i32) (result i32))
(func (import "wasi_unstable" "fd_prestat_dir_name") (param i32 i32 i32) (result i32))
(func (import "wasi_unstable" "fd_pwrite") (param i32 i32 i32 i64 i32) (result i32))
(func (import "wasi_unstable" "fd_read") (param i32 i32 i32 i32) (result i32))
(func (import "wasi_unstable" "fd_readdir") (param i32 i32 i32 i64 i32) (result i32))
(func (import "wasi_unstable" "fd_renumber") (param i32 i32) (result i32))
(func (import "wasi_unstable" "fd_seek") (param i32 i64 i32 i32) (result i32))
(func (import "wasi_unstable" "fd_sync") (param i32) (result i32))
(func (import "wasi_unstable" "fd_tell") (param i32 i32) (result i32))
(func (import "wasi_unstable" "fd_write") (param i32 i32 i32 i32) (result i32))
(func (import "wasi_unstable" "path_create_directory") (param i32 i32 i32) (result i32))
(func (import "wasi_unstable" "path_filestat_get") (param i32 i32 i32 i32 i32) (result i32))
(func (import "wasi_unstable" "path_filestat_set_times") (param i32 i32 i32 i32 i64 i64 i32) (result i32))
(func (import "wasi_unstable" "path_link") (param i32 i32 i32 i32 i32 i32 i32) (result i32))
(func (import "wasi_unstable" "path_open") (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32))
(func (import "wasi_unstable" "path_readlink") (param i32 i32 i32 i32 i32 i32) (result i32))
(func (import "wasi_unstable" "path_remove_directory") (param i32 i32 i32) (result i32))
(func (import "wasi_unstable" "path_rename") (param i32 i32 i32 i32 i32 i32) (result i32))
(func (import "wasi_unstable" "path_symlink") (param i32 i32 i32 i32 i32) (result i32))
(func (import "wasi_unstable" "path_unlink_file") (param i32 i32 i32) (result i32))
(func (import "wasi_unstable" "poll_oneoff") (param i32 i32 i32 i32) (result i32))
(func (import "wasi_unstable" "proc_exit") (param i32))
(func (import "wasi_unstable" "proc_raise") (param i32) (result i32))
(func (import "wasi_unstable" "random_get") (param i32 i32) (result i32))
(func (import "wasi_unstable" "sched_yield") (result i32))
(func (import "wasi_unstable" "sock_recv") (param i32 i32 i32 i32 i32 i32) (result i32))
(func (import "wasi_unstable" "sock_send") (param i32 i32 i32 i32 i32) (result i32))
(func (import "wasi_unstable" "sock_shutdown") (param i32 i32) (result i32))
)
```
Notes:
- multiple `assert-import` and `assert-export` declarations are allowed.
- comments (starts with `;` and ends with a newline) and whitespace are valid between any tokens
## Semantics
All imports used by the module must be specified in the interface.
All exports in the interface must be exported by the module.
Thus the module may have additional exports than the interface or fewer imports than the interface specifies and be considered valid.
## Misc
Wasm Interface serves a slightly different purpose than the proposed WebIDL for Wasm standard, but may be replaced by it in the future if things change.
Due to an issue with nested closures in Rust, `wasm-interface` can't both compile on stable and have good error reporting. This is being fixed and `wasm-interface` will be updated to have better error handling.
See the `parser.rs` file for a comment containing the grammar in a BNF style.
Suggestions, contributions, and thoughts welcome! This is an experiment in the early stages, but we hope to work with the wider community and develop this in cooperation with all interested parties.

View File

@ -0,0 +1,205 @@
//! The definition of a WASM interface
use crate::interface_matcher::InterfaceMatcher;
use serde::{Deserialize, Serialize};
use std::collections::{hash_map::Entry, HashMap, HashSet};
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Interface {
/// The name the interface gave itself
pub name: Option<String>,
/// Things that the module can import
pub imports: HashMap<(String, String), Import>,
/// Things that the module must export
pub exports: HashMap<String, Export>,
}
impl Interface {
pub fn merge(&self, other: Interface) -> Result<Interface, String> {
let mut base = self.clone();
for (key, val) in other.imports {
match base.imports.entry(key) {
Entry::Occupied(e) if *e.get() != val => {
let (namespace, name) = e.key();
let original_value = e.get();
return Err(format!("Conflict detected: the import \"{namespace}\" \"{name}\" was found but the definitions were different: {original_value:?} {val:?}"));
}
Entry::Occupied(_) => {
// it's okay for the imported items to be the same.
}
Entry::Vacant(e) => {
e.insert(val);
}
};
}
for (key, val) in other.exports {
match base.exports.entry(key) {
Entry::Occupied(e) if *e.get() != val => {
let name = e.key();
let original_value = e.get();
return Err(format!("Conflict detected: the key \"{name}\" was found in exports but the definitions were different: {original_value:?} {val:?}"));
}
Entry::Occupied(_) => {
// it's okay for the exported items to be the same.
}
Entry::Vacant(e) => {
e.insert(val);
}
};
}
Ok(base)
}
pub fn create_interface_matcher(&self) -> InterfaceMatcher {
let mut namespaces = HashSet::new();
let mut namespace_imports: HashMap<String, HashSet<Import>> =
HashMap::with_capacity(self.imports.len());
let mut exports = HashSet::with_capacity(self.exports.len());
for (_, import) in self.imports.iter() {
match import {
Import::Func { namespace, .. } | Import::Global { namespace, .. } => {
if !namespaces.contains(namespace) {
namespaces.insert(namespace.clone());
}
let ni = namespace_imports.entry(namespace.clone()).or_default();
ni.insert(import.clone());
}
}
}
for (_, export) in self.exports.iter() {
exports.insert(export.clone());
}
InterfaceMatcher {
namespaces,
namespace_imports,
exports,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Import {
Func {
namespace: String,
name: String,
params: Vec<WasmType>,
result: Vec<WasmType>,
},
Global {
namespace: String,
name: String,
var_type: WasmType,
},
}
impl Import {
pub fn format_key(ns: &str, name: &str) -> (String, String) {
(ns.to_string(), name.to_string())
}
/// Get the key used to look this import up in the Interface's import hashmap
pub fn get_key(&self) -> (String, String) {
match self {
Import::Func {
namespace, name, ..
}
| Import::Global {
namespace, name, ..
} => Self::format_key(namespace, name),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Export {
Func {
name: String,
params: Vec<WasmType>,
result: Vec<WasmType>,
},
Global {
name: String,
var_type: WasmType,
},
}
impl Export {
pub fn format_key(name: &str) -> String {
name.to_string()
}
/// Get the key used to look this export up in the Interface's export hashmap
pub fn get_key(&self) -> String {
match self {
Export::Func { name, .. } | Export::Global { name, .. } => Self::format_key(name),
}
}
}
/// Primitive wasm type
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum WasmType {
I32,
I64,
F32,
F64,
}
impl std::fmt::Display for WasmType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
WasmType::I32 => "i32",
WasmType::I64 => "i64",
WasmType::F32 => "f32",
WasmType::F64 => "f64",
}
)
}
}
#[cfg(test)]
mod test {
use crate::parser;
#[test]
fn merging_works() {
let interface1_src =
r#"(interface (func (import "env" "plus_one") (param i32) (result i32)))"#;
let interface2_src =
r#"(interface (func (import "env" "plus_one") (param i64) (result i64)))"#;
let interface3_src =
r#"(interface (func (import "env" "times_two") (param i64) (result i64)))"#;
let interface4_src =
r#"(interface (func (import "env" "times_two") (param i64 i64) (result i64)))"#;
let interface5_src = r#"(interface (func (export "empty_bank_account") (param) (result)))"#;
let interface6_src =
r#"(interface (func (export "empty_bank_account") (param) (result i64)))"#;
let interface1 = parser::parse_interface(interface1_src).unwrap();
let interface2 = parser::parse_interface(interface2_src).unwrap();
let interface3 = parser::parse_interface(interface3_src).unwrap();
let interface4 = parser::parse_interface(interface4_src).unwrap();
let interface5 = parser::parse_interface(interface5_src).unwrap();
let interface6 = parser::parse_interface(interface6_src).unwrap();
assert!(interface1.merge(interface2.clone()).is_err());
assert!(interface2.merge(interface1.clone()).is_err());
assert!(interface1.merge(interface3.clone()).is_ok());
assert!(interface2.merge(interface3.clone()).is_ok());
assert!(interface3.merge(interface2).is_ok());
assert!(
interface1.merge(interface1.clone()).is_ok(),
"exact matches are accepted"
);
assert!(interface3.merge(interface4).is_err());
assert!(interface5.merge(interface5.clone()).is_ok());
assert!(interface5.merge(interface6).is_err());
}
}

View File

@ -0,0 +1,29 @@
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use crate::interface::{Export, Import};
/// A struct containing data for more efficient matching.
///
/// An ideal use case for this is to parse [`Interface`]s at compile time,
/// create [`InterfaceMatcher`]s, and store them as bytes so that they
/// can be efficiently loaded at runtime for matching.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct InterfaceMatcher {
pub namespaces: HashSet<String>,
pub namespace_imports: HashMap<String, HashSet<Import>>,
pub exports: HashSet<Export>,
}
#[cfg(feature = "binary_encode")]
impl InterfaceMatcher {
/// Store the matcher as bytes to avoid reparsing
fn into_bytes(&self) -> Vec<u8> {
bincode::serialize(self).expect("Could not serialize InterfaceMatcher")
}
/// Load the matcher from bytes to avoid reparsing
fn from_bytes(bytes: &[u8]) -> Option<Self> {
bincode::deserialize(bytes).ok()
}
}

View File

@ -0,0 +1,13 @@
#![type_length_limit = "5795522"]
//! Definition and parsing of wasm interfaces
//!
//! wasm interfaces ensure wasm modules conform to a specific shape
//! they do this by asserting on the imports and exports of the module.
pub mod interface;
pub mod interface_matcher;
pub mod parser;
#[cfg(feature = "validation")]
pub mod validate;
pub use interface::*;

View File

@ -0,0 +1,596 @@
//! Parsers to get a wasm interface from text
//!
//! The grammar of the text format is:
//! interface = "(" interface name? interface-entry* ")"
//! interface-entry = func | global
//!
//! func = import-fn | export-fn
//! global = import-global | export-global
//!
//! import-fn = "(" "func" import-id param-list? result-list? ")"
//! import-global = "(" "global" import-id type-decl ")"
//! import-id = "(" "import" namespace name ")"
//!
//! export-fn = "(" "func" export-id param-list? result-list? ")"
//! export-global = "(" "global" export-id type-decl ")"
//! export-id = "(" export name ")"
//!
//! param-list = "(" param type* ")"
//! result-list = "(" result type* ")"
//! type-decl = "(" "type" type ")"
//! namespace = "\"" identifier "\""
//! name = "\"" identifier "\""
//! identifier = any character that's not a whitespace character or an open or close parenthesis
//! type = "i32" | "i64" | "f32" | "f64"
//!
//! + means 1 or more
//! * means 0 or more
//! ? means 0 or 1
//! | means "or"
//! "\"" means one `"` character
//!
//! comments start with a `;` character and go until a newline `\n` character is reached
//! comments and whitespace are valid between any tokens
use either::Either;
use nom::{
branch::*,
bytes::complete::{escaped, is_not, tag},
character::complete::{char, multispace0, multispace1, one_of},
combinator::*,
error::context,
multi::many0,
sequence::{delimited, preceded, tuple},
IResult,
};
use crate::interface::*;
/// Some example input:
/// (interface "example_interface"
/// (func (import "ns" "name") (param f64 i32) (result f64 i32))
/// (func (export "name") (param f64 i32) (result f64 i32))
/// (global (import "ns" "name") (type f64)))
pub fn parse_interface(mut input: &str) -> Result<Interface, String> {
let mut interface = Interface::default();
let interface_inner = preceded(
tag("interface"),
tuple((
opt(preceded(space_comments, identifier)),
many0(parse_func_or_global),
)),
);
let interface_parser = preceded(space_comments, s_exp(interface_inner));
if let Result::Ok((inp, (sig_id, out))) = interface_parser(input) {
interface.name = sig_id.map(|s_id| s_id.to_string());
for entry in out.into_iter() {
match entry {
Either::Left(import) => {
if let Some(dup) = interface.imports.insert(import.get_key(), import) {
return Err(format!("Duplicate import found {:?}", dup));
}
}
Either::Right(export) => {
if let Some(dup) = interface.exports.insert(export.get_key(), export) {
return Err(format!("Duplicate export found {:?}", dup));
}
}
}
}
input = inp;
}
// catch trailing comments and spaces
if let Ok((inp, _)) = space_comments(input) {
input = inp;
}
if !input.is_empty() {
Err(format!("Could not parse remaining input: {}", input))
} else {
Ok(interface)
}
}
fn parse_comment(input: &str) -> IResult<&str, ()> {
map(
preceded(multispace0, preceded(char(';'), many0(is_not("\n")))),
|_| (),
)(input)
}
/// Consumes spaces and comments
/// comments must terminate with a new line character
fn space_comments<'a>(mut input: &'a str) -> IResult<&'a str, ()> {
let mut space_found = true;
let mut comment_found = true;
while space_found || comment_found {
let space: IResult<&'a str, _> = multispace1(input);
space_found = if let Result::Ok((inp, _)) = space {
input = inp;
true
} else {
false
};
comment_found = if let Result::Ok((inp, _)) = parse_comment(input) {
input = inp;
true
} else {
false
};
}
Ok((input, ()))
}
/// A quoted identifier, must be valid UTF8
fn identifier(input: &str) -> IResult<&str, &str> {
let name_inner = escaped(is_not("\"\\"), '\\', one_of("\"n\\"));
context("identifier", delimited(char('"'), name_inner, char('"')))(input)
}
/// Parses a wasm primitive type
fn wasm_type(input: &str) -> IResult<&str, WasmType> {
let i32_tag = map(tag("i32"), |_| WasmType::I32);
let i64_tag = map(tag("i64"), |_| WasmType::I64);
let f32_tag = map(tag("f32"), |_| WasmType::F32);
let f64_tag = map(tag("f64"), |_| WasmType::F64);
alt((i32_tag, i64_tag, f32_tag, f64_tag))(input)
}
/// Parses an S-expression
fn s_exp<'a, O1, F>(inner: F) -> impl Fn(&'a str) -> IResult<&'a str, O1>
where
F: Fn(&'a str) -> IResult<&'a str, O1>,
{
delimited(
char('('),
preceded(space_comments, inner),
preceded(space_comments, char(')')),
)
}
fn parse_func_or_global(input: &str) -> IResult<&str, Either<Import, Export>> {
preceded(space_comments, alt((func, global)))(input)
}
/// (func (import "ns" "name") (param f64 i32) (result f64 i32))
/// (func (export "name") (param f64 i32) (result f64 i32))
fn func(input: &str) -> IResult<&str, Either<Import, Export>> {
let param_list_inner = preceded(tag("param"), many0(preceded(space_comments, wasm_type)));
let param_list = opt(s_exp(param_list_inner));
let result_list_inner = preceded(tag("result"), many0(preceded(space_comments, wasm_type)));
let result_list = opt(s_exp(result_list_inner));
let import_id_inner = preceded(
tag("import"),
tuple((
preceded(space_comments, identifier),
preceded(space_comments, identifier),
)),
);
let export_id_inner = preceded(tag("export"), preceded(space_comments, identifier));
let func_id_inner = alt((
map(import_id_inner, |(ns, name)| {
Either::Left((ns.to_string(), name.to_string()))
}),
map(export_id_inner, |name| Either::Right(name.to_string())),
));
let func_id = s_exp(func_id_inner);
let func_import_inner = context(
"func import inner",
preceded(
tag("func"),
map(
tuple((
preceded(space_comments, func_id),
preceded(space_comments, param_list),
preceded(space_comments, result_list),
)),
|(func_id, pl, rl)| match func_id {
Either::Left((ns, name)) => Either::Left(Import::Func {
namespace: ns,
name,
params: pl.unwrap_or_default(),
result: rl.unwrap_or_default(),
}),
Either::Right(name) => Either::Right(Export::Func {
name,
params: pl.unwrap_or_default(),
result: rl.unwrap_or_default(),
}),
},
),
),
);
s_exp(func_import_inner)(input)
}
/// (global (import "ns" "name") (type f64))
/// (global (export "name") (type f64))
fn global(input: &str) -> IResult<&str, Either<Import, Export>> {
let global_type_inner = preceded(tag("type"), preceded(space_comments, wasm_type));
let type_s_exp = s_exp(global_type_inner);
let export_inner = preceded(tag("export"), preceded(space_comments, identifier));
let import_inner = preceded(
tag("import"),
tuple((
preceded(space_comments, identifier),
preceded(space_comments, identifier),
)),
);
let global_id_inner = alt((
map(import_inner, |(ns, name)| {
Either::Left(Import::Global {
namespace: ns.to_string(),
name: name.to_string(),
// placeholder type, overwritten in `global_inner`
var_type: WasmType::I32,
})
}),
map(export_inner, |name| {
Either::Right(Export::Global {
name: name.to_string(),
// placeholder type, overwritten in `global_inner`
var_type: WasmType::I32,
})
}),
));
let global_id = s_exp(global_id_inner);
let global_inner = context(
"global inner",
preceded(
tag("global"),
map(
tuple((
preceded(space_comments, global_id),
preceded(space_comments, type_s_exp),
)),
|(import_or_export, var_type)| match import_or_export {
Either::Left(Import::Global {
namespace, name, ..
}) => Either::Left(Import::Global {
namespace,
name,
var_type,
}),
Either::Right(Export::Global { name, .. }) => {
Either::Right(Export::Global { name, var_type })
}
_ => unreachable!("Invalid value interonally in parse global function"),
},
),
),
);
s_exp(global_inner)(input)
}
#[cfg(test)]
mod test {
use super::*;
use std::collections::HashMap;
#[test]
fn parse_wasm_type() {
let i32_res = wasm_type("i32").unwrap();
assert_eq!(i32_res, ("", WasmType::I32));
let i64_res = wasm_type("i64").unwrap();
assert_eq!(i64_res, ("", WasmType::I64));
let f32_res = wasm_type("f32").unwrap();
assert_eq!(f32_res, ("", WasmType::F32));
let f64_res = wasm_type("f64").unwrap();
assert_eq!(f64_res, ("", WasmType::F64));
assert!(wasm_type("i128").is_err());
}
#[test]
fn parse_identifier() {
let inner_str = "柴は可愛すぎるだと思います";
let input = format!("\"{}\"", &inner_str);
let parse_res = identifier(&input).unwrap();
assert_eq!(parse_res, ("", inner_str))
}
#[test]
fn parse_global_import() {
let parse_res = global(r#"(global (import "env" "length") (type i32))"#)
.ok()
.and_then(|(a, b)| Some((a, b.left()?)))
.unwrap();
assert_eq!(
parse_res,
(
"",
Import::Global {
namespace: "env".to_string(),
name: "length".to_string(),
var_type: WasmType::I32,
}
)
);
}
#[test]
fn parse_global_export() {
let parse_res = global(r#"(global (export "length") (type i32))"#)
.ok()
.and_then(|(a, b)| Some((a, b.right()?)))
.unwrap();
assert_eq!(
parse_res,
(
"",
Export::Global {
name: "length".to_string(),
var_type: WasmType::I32,
}
)
);
}
#[test]
fn parse_func_import() {
let parse_res = func(r#"(func (import "ns" "name") (param f64 i32) (result f64 i32))"#)
.ok()
.and_then(|(a, b)| Some((a, b.left()?)))
.unwrap();
assert_eq!(
parse_res,
(
"",
Import::Func {
namespace: "ns".to_string(),
name: "name".to_string(),
params: vec![WasmType::F64, WasmType::I32],
result: vec![WasmType::F64, WasmType::I32],
}
)
);
}
#[test]
fn parse_func_export() {
let parse_res = func(r#"(func (export "name") (param f64 i32) (result f64 i32))"#)
.ok()
.and_then(|(a, b)| Some((a, b.right()?)))
.unwrap();
assert_eq!(
parse_res,
(
"",
Export::Func {
name: "name".to_string(),
params: vec![WasmType::F64, WasmType::I32],
result: vec![WasmType::F64, WasmType::I32],
}
)
);
let parse_res = func(r#"(func (export "name"))"#)
.ok()
.and_then(|(a, b)| Some((a, b.right()?)))
.unwrap();
assert_eq!(
parse_res,
(
"",
Export::Func {
name: "name".to_string(),
params: vec![],
result: vec![],
}
)
)
}
#[test]
fn parse_imports_test() {
let parse_imports = |in_str| {
many0(parse_func_or_global)(in_str)
.map(|(a, b)| {
(
a,
b.into_iter().filter_map(|x| x.left()).collect::<Vec<_>>(),
)
})
.unwrap()
};
let parse_res =
parse_imports(r#"(func (import "ns" "name") (param f64 i32) (result f64 i32))"#);
assert_eq!(
parse_res,
(
"",
vec![Import::Func {
namespace: "ns".to_string(),
name: "name".to_string(),
params: vec![WasmType::F64, WasmType::I32],
result: vec![WasmType::F64, WasmType::I32],
}]
)
);
let parse_res = parse_imports(
r#"(func (import "ns" "name")
(param f64 i32) (result f64 i32))
( global ( import "env" "length" ) ( type
;; i32 is the best type
i32 )
)
(func (import "ns" "name2") (param f32
i64)
;; The return value comes next
(
result
f64
i32
)
)"#,
);
assert_eq!(
parse_res,
(
"",
vec![
Import::Func {
namespace: "ns".to_string(),
name: "name".to_string(),
params: vec![WasmType::F64, WasmType::I32],
result: vec![WasmType::F64, WasmType::I32],
},
Import::Global {
namespace: "env".to_string(),
name: "length".to_string(),
var_type: WasmType::I32,
},
Import::Func {
namespace: "ns".to_string(),
name: "name2".to_string(),
params: vec![WasmType::F32, WasmType::I64],
result: vec![WasmType::F64, WasmType::I32],
},
]
)
);
}
#[test]
fn top_level_test() {
let parse_res = parse_interface(
r#" (interface
(func (import "ns" "name") (param f64 i32) (result f64 i32))
(func (export "name2") (param) (result i32))
(global (import "env" "length") (type f64)))"#,
)
.unwrap();
let imports = vec![
Import::Func {
namespace: "ns".to_string(),
name: "name".to_string(),
params: vec![WasmType::F64, WasmType::I32],
result: vec![WasmType::F64, WasmType::I32],
},
Import::Global {
namespace: "env".to_string(),
name: "length".to_string(),
var_type: WasmType::F64,
},
];
let exports = vec![Export::Func {
name: "name2".to_string(),
params: vec![],
result: vec![WasmType::I32],
}];
let import_map = imports
.into_iter()
.map(|entry| (entry.get_key(), entry))
.collect::<HashMap<(String, String), Import>>();
let export_map = exports
.into_iter()
.map(|entry| (entry.get_key(), entry))
.collect::<HashMap<String, Export>>();
assert_eq!(
parse_res,
Interface {
name: None,
imports: import_map,
exports: export_map,
}
);
}
#[test]
fn duplicates_not_allowed() {
let parse_res = parse_interface(
r#" (interface "sig_name" (func (import "ns" "name") (param f64 i32) (result f64 i32))
; test comment
;; hello
(func (import "ns" "name") (param) (result i32))
(global (export "length") (type f64)))
"#,
);
assert!(parse_res.is_err());
}
#[test]
fn test_comment_space_parsing() {
let parse_res = space_comments(" ").unwrap();
assert_eq!(parse_res, ("", ()));
let parse_res = space_comments("").unwrap();
assert_eq!(parse_res, ("", ()));
let parse_res = space_comments("; hello\n").unwrap();
assert_eq!(parse_res, ("", ()));
let parse_res = space_comments("abc").unwrap();
assert_eq!(parse_res, ("abc", ()));
let parse_res = space_comments("\n ; hello\n ").unwrap();
assert_eq!(parse_res, ("", ()));
let parse_res = space_comments("\n ; hello\n ; abc\n\n ; hello\n").unwrap();
assert_eq!(parse_res, ("", ()));
}
#[test]
fn test_param_elision() {
let parse_res = parse_interface(
r#" (interface "interface_name" (func (import "ns" "name") (result f64 i32))
(func (export "name")))
"#,
)
.unwrap();
let imports = vec![Import::Func {
namespace: "ns".to_string(),
name: "name".to_string(),
params: vec![],
result: vec![WasmType::F64, WasmType::I32],
}];
let exports = vec![Export::Func {
name: "name".to_string(),
params: vec![],
result: vec![],
}];
let import_map = imports
.into_iter()
.map(|entry| (entry.get_key(), entry))
.collect::<HashMap<(String, String), Import>>();
let export_map = exports
.into_iter()
.map(|entry| (entry.get_key(), entry))
.collect::<HashMap<String, Export>>();
assert_eq!(
parse_res,
Interface {
name: Some("interface_name".to_string()),
imports: import_map,
exports: export_map,
}
);
}
#[test]
fn typo_gets_caught() {
let interface_src = r#"
(interface "interface_id"
(func (import "env" "do_panic") (params i32 i64))
(global (import "length") (type i32)))"#;
let result = parse_interface(interface_src);
assert!(result.is_err());
}
#[test]
fn parse_trailing_spaces_on_interface() {
let parse_res = parse_interface(
r#" (interface "really_good_interface" (func (import "ns" "name") (param f64 i32) (result f64 i32))
; test comment
;; hello
(global (import "ns" "length") (type f64))
)
"#,
);
assert!(parse_res.is_ok());
}
}

View File

@ -0,0 +1,465 @@
//! Validate a wasm module given a interface.
//!
//! This checks that all imports are specified in the interface and that their types
//! are correct, as well as that all exports that the interface expects are exported
//! by the module and that their types are correct.
use crate::{Export, Import, Interface, WasmType};
use std::collections::HashMap;
use wasmparser::{ExternalKind, FuncType, GlobalType, ImportSectionEntryType};
pub fn validate_wasm_and_report_errors(
wasm: &[u8],
interface: &Interface,
) -> Result<(), WasmValidationError> {
use wasmparser::WasmDecoder;
let mut errors: Vec<String> = vec![];
let mut import_fns: HashMap<(String, String), u32> = HashMap::new();
let mut export_fns: HashMap<String, u32> = HashMap::new();
let mut export_globals: HashMap<String, u32> = HashMap::new();
let mut type_defs: Vec<FuncType> = vec![];
let mut global_types: Vec<GlobalType> = vec![];
let mut fn_sigs: Vec<u32> = vec![];
let mut parser = wasmparser::ValidatingParser::new(wasm, None);
loop {
let state = parser.read();
match state {
wasmparser::ParserState::EndWasm => break,
wasmparser::ParserState::Error(e) => {
return Err(WasmValidationError::InvalidWasm {
error: format!("{}", e),
});
}
wasmparser::ParserState::ImportSectionEntry {
module,
field,
ref ty,
} => match ty {
ImportSectionEntryType::Function(idx) => {
import_fns.insert(Import::format_key(module, field), *idx);
fn_sigs.push(*idx);
}
ImportSectionEntryType::Global(GlobalType { content_type, .. }) => {
let global_type =
wasmparser_type_into_wasm_type(*content_type).map_err(|err| {
WasmValidationError::UnsupportedType {
error: format!(
"Invalid type found in import \"{}\" \"{}\": {}",
module, field, err
),
}
})?;
if let Some(val) = interface.imports.get(&Import::format_key(module, field)) {
if let Import::Global { var_type, .. } = val {
if *var_type != global_type {
errors.push(format!(
"Invalid type on Global \"{}\". Expected {} found {}",
field, var_type, global_type
));
}
} else {
errors.push(format!(
"Invalid import type. Expected Global, found {:?}",
val
));
}
} else {
errors.push(format!(
"Global import \"{}\" not found in the specified interface",
field
));
}
}
_ => (),
},
wasmparser::ParserState::ExportSectionEntry {
field,
index,
ref kind,
} => match kind {
ExternalKind::Function => {
export_fns.insert(Export::format_key(field), *index);
}
ExternalKind::Global => {
export_globals.insert(Export::format_key(field), *index);
}
_ => (),
},
wasmparser::ParserState::BeginGlobalSectionEntry(gt) => {
global_types.push(*gt);
}
wasmparser::ParserState::TypeSectionEntry(ft) => {
type_defs.push(ft.clone());
}
wasmparser::ParserState::FunctionSectionEntry(n) => {
fn_sigs.push(*n);
}
_ => {}
}
}
validate_imports(&import_fns, &type_defs, interface, &mut errors);
validate_export_fns(&export_fns, &type_defs, &fn_sigs, interface, &mut errors);
validate_export_globals(&export_globals, &global_types, interface, &mut errors);
if errors.is_empty() {
Ok(())
} else {
Err(WasmValidationError::InterfaceViolated { errors })
}
}
/// Validates the import functions, checking the name and type against the given
/// `Interface`
fn validate_imports(
import_fns: &HashMap<(String, String), u32>,
type_defs: &[FuncType],
interface: &Interface,
errors: &mut Vec<String>,
) {
for (key, val) in import_fns.iter() {
if let Some(interface_def) = interface.imports.get(key) {
let type_sig = if let Some(v) = type_defs.get(*val as usize) {
v
} else {
errors.push(format!(
"Use of undeclared function reference \"{}\" in import function \"{}\" \"{}\"",
val, key.0, key.1
));
continue;
};
if let Import::Func { params, result, .. } = interface_def {
debug_assert!(type_sig.form == wasmparser::Type::Func);
for (i, param) in type_sig
.params
.iter()
.cloned()
.map(wasmparser_type_into_wasm_type)
.enumerate()
{
match param {
Ok(t) => {
if params.get(i).is_none() {
errors.push(format!("Found {} args but the interface only expects {} for imported function \"{}\" \"{}\"", i, params.len(), &key.0, &key.1));
continue;
}
if t != params[i] {
errors.push(format!(
"Type mismatch in params in imported func \"{}\" \"{}\": argument {}, expected {} found {}",
&key.0, &key.1, i + 1, params[i], t
));
}
}
Err(e) => errors.push(format!(
"Invalid type in func \"{}\" \"{}\": {}",
&key.0, &key.1, e
)),
}
}
for (i, ret) in type_sig
.returns
.iter()
.cloned()
.map(wasmparser_type_into_wasm_type)
.enumerate()
{
match ret {
Ok(t) => {
if result.get(i).is_none() {
errors.push(format!("Found {} returns but the interface only expects {} for imported function \"{}\" \"{}\"", i, params.len(), &key.0, &key.1));
continue;
}
if t != result[i] {
errors.push(format!(
"Type mismatch in returns in func \"{}\" \"{}\", return {}, expected {} found {}",
&key.0, &key.1, i + 1, params[i], t
));
}
}
Err(e) => errors.push(format!(
"Invalid type in func \"{}\" \"{}\": {}",
&key.0, &key.1, e
)),
}
}
}
} else {
// we didn't find the import at all in the interface
// TODO: improve error messages by including type information
errors.push(format!("Missing import \"{}\" \"{}\"", key.0, key.1));
}
}
}
/// Validates the export functions, checking the name and type against the given
/// `Interface`
fn validate_export_fns(
export_fns: &HashMap<String, u32>,
type_defs: &[FuncType],
fn_sigs: &Vec<u32>,
interface: &Interface,
errors: &mut Vec<String>,
) {
'export_loop: for (key, val) in export_fns.iter() {
if let Some(interface_def) = interface.exports.get(key) {
let type_sig = if let Some(type_idx) = fn_sigs.get(*val as usize) {
if let Some(v) = type_defs.get(*type_idx as usize) {
v
} else {
errors.push(format!(
"Export \"{}\" refers to type \"{}\" but only {} types were found",
&key,
type_idx,
fn_sigs.len()
));
continue;
}
} else {
errors.push(format!(
"Use of undeclared function reference \"{}\" in export \"{}\"",
val, &key
));
continue;
};
if let Export::Func { params, result, .. } = interface_def {
debug_assert!(type_sig.form == wasmparser::Type::Func);
for (i, param) in type_sig
.params
.iter()
.cloned()
.map(wasmparser_type_into_wasm_type)
.enumerate()
{
match param {
Ok(t) => {
if params.get(i).is_none() {
errors.push(format!("Found {} args but the interface only expects {} for exported function \"{}\"", type_sig.params.len(), params.len(), &key));
continue 'export_loop;
}
if t != params[i] {
errors.push(format!(
"Type mismatch in params in exported func \"{}\": in argument {}, expected {} found {}",
&key, i + 1, params[i], t
));
}
}
Err(e) => errors
.push(format!("Invalid type in exported func \"{}\": {}", &key, e)),
}
}
for (i, ret) in type_sig
.returns
.iter()
.cloned()
.map(wasmparser_type_into_wasm_type)
.enumerate()
{
match ret {
Ok(t) => {
if result.get(i).is_none() {
errors.push(format!("Found {} returns but the interface only expects {} for exported function \"{}\"", i, params.len(), &key));
continue 'export_loop;
}
if t != result[i] {
errors.push(format!(
"Type mismatch in returns in exported func \"{}\": in return {}, expected {} found {}",
&key, i + 1, result[i], t
));
}
}
Err(e) => errors
.push(format!("Invalid type in exported func \"{}\": {}", &key, e)),
}
}
}
}
}
}
/// Validates the export globals, checking the name and type against the given
/// `Interface`
fn validate_export_globals(
export_globals: &HashMap<String, u32>,
global_types: &Vec<GlobalType>,
interface: &Interface,
errors: &mut Vec<String>,
) {
for (key, val) in export_globals.iter() {
if let Some(Export::Global { var_type, .. }) = interface.exports.get(key) {
if global_types.get(*val as usize).is_none() {
errors.push(format!(
"Invalid wasm, expected {} global types, found {}",
val,
global_types.len()
));
}
match wasmparser_type_into_wasm_type(global_types[*val as usize].content_type) {
Ok(t) => {
if *var_type != t {
errors.push(format!(
"Type mismatch in global export {}: expected {} found {}",
&key, var_type, t
));
}
}
Err(e) => errors.push(format!("In global export {}: {}", &key, e)),
}
}
}
}
/// Converts Wasmparser's type enum into wasm-interface's type enum
/// wasmparser's enum contains things which are invalid in many situations
///
/// Additionally wasmerparser containers more advanced types like references that
/// wasm-interface does not yet support
fn wasmparser_type_into_wasm_type(ty: wasmparser::Type) -> Result<WasmType, String> {
use wasmparser::Type;
Ok(match ty {
Type::I32 => WasmType::I32,
Type::I64 => WasmType::I64,
Type::F32 => WasmType::F32,
Type::F64 => WasmType::F64,
e => {
return Err(format!("Invalid type found: {:?}", e));
}
})
}
#[cfg(test)]
mod validation_tests {
use super::*;
use crate::parser;
#[test]
fn global_imports() {
const WAT: &str = r#"(module
(type $t0 (func (param i32 i64)))
(global $length (import "env" "length") i32)
(import "env" "do_panic" (func $do_panic (type $t0)))
)"#;
let wasm = wat::parse_str(WAT).unwrap();
let interface_src = r#"
(interface
(func (import "env" "do_panic") (param i32 i64))
(global (import "env" "length") (type i32)))"#;
let interface = parser::parse_interface(interface_src).unwrap();
let result = validate_wasm_and_report_errors(&wasm[..], &interface);
assert!(result.is_ok());
// Now set the global import type to mismatch the wasm
let interface_src = r#"
(interface
(func (import "env" "do_panic") (param i32 i64))
(global (import "env" "length") (type i64)))"#;
let interface = parser::parse_interface(interface_src).unwrap();
let result = validate_wasm_and_report_errors(&wasm[..], &interface);
assert!(
result.is_err(),
"global import type mismatch causes an error"
);
// Now set the function import type to mismatch the wasm
let interface_src = r#"
(interface
(func (import "env" "do_panic") (param i64))
(global (import "env" "length") (type i32)))"#;
let interface = parser::parse_interface(interface_src).unwrap();
let result = validate_wasm_and_report_errors(&wasm[..], &interface);
assert!(
result.is_err(),
"function import type mismatch causes an error"
);
// Now try with a module that has an import that the interface doesn't have
let interface_src = r#"
(interface
(func (import "env" "do_panic") (param i64))
(global (import "env" "length_plus_plus") (type i32)))"#;
let interface = parser::parse_interface(interface_src).unwrap();
let result = validate_wasm_and_report_errors(&wasm[..], &interface);
assert!(
result.is_err(),
"all imports must be covered by the interface"
);
}
#[test]
fn global_exports() {
const WAT: &str = r#"(module
(func (export "as-set_local-first") (param i32) (result i32)
(nop) (i32.const 2) (set_local 0) (get_local 0))
(global (export "num_tries") i64 (i64.const 0))
)"#;
let wasm = wat::parse_str(WAT).unwrap();
let interface_src = r#"
(interface
(func (export "as-set_local-first") (param i32) (result i32))
(global (export "num_tries") (type i64)))"#;
let interface = parser::parse_interface(interface_src).unwrap();
let result = validate_wasm_and_report_errors(&wasm[..], &interface);
assert!(result.is_ok());
// Now set the global export type to mismatch the wasm
let interface_src = r#"
(interface
(func (export "as-set_local-first") (param i32) (result i32))
(global (export "num_tries") (type f32)))"#;
let interface = parser::parse_interface(interface_src).unwrap();
let result = validate_wasm_and_report_errors(&wasm[..], &interface);
assert!(
result.is_err(),
"global export type mismatch causes an error"
);
// Now set the function export type to mismatch the wasm
let interface_src = r#"
(interface
(func (export "as-set_local-first") (param i64) (result i64))
(global (export "num_tries") (type i64)))"#;
let interface = parser::parse_interface(interface_src).unwrap();
let result = validate_wasm_and_report_errors(&wasm[..], &interface);
assert!(
result.is_err(),
"function export type mismatch causes an error"
);
// Now try a interface that requires an export that the module doesn't have
let interface_src = r#"
(interface
(func (export "as-set_local-first") (param i64) (result i64))
(global (export "numb_trees") (type i64)))"#;
let interface = parser::parse_interface(interface_src).unwrap();
let result = validate_wasm_and_report_errors(&wasm[..], &interface);
assert!(result.is_err(), "missing a required export is an error");
}
}
#[derive(Debug)]
pub enum WasmValidationError {
InvalidWasm { error: String },
InterfaceViolated { errors: Vec<String> },
UnsupportedType { error: String },
}

View File

@ -419,6 +419,6 @@ fn large_number_local(mut config: crate::Config) -> Result<()> {
.get_function("large_local")? .get_function("large_local")?
.call(&mut store, &[]) .call(&mut store, &[])
.unwrap(); .unwrap();
assert_eq!(&Value::I64(1 as i64), result.get(0).unwrap()); assert_eq!(&Value::I64(1_i64), result.get(0).unwrap());
Ok(()) Ok(())
} }

View File

@ -246,11 +246,11 @@ fn static_host_function_without_env(config: crate::Config) -> anyhow::Result<()>
let mut store = config.store(); let mut store = config.store();
fn f(a: i32, b: i64, c: f32, d: f64) -> (f64, f32, i64, i32) { fn f(a: i32, b: i64, c: f32, d: f64) -> (f64, f32, i64, i32) {
(d * 4.0, c * 3.0, b * 2, a * 1) (d * 4.0, c * 3.0, b * 2, a)
} }
fn f_ok(a: i32, b: i64, c: f32, d: f64) -> Result<(f64, f32, i64, i32), Infallible> { fn f_ok(a: i32, b: i64, c: f32, d: f64) -> Result<(f64, f32, i64, i32), Infallible> {
Ok((d * 4.0, c * 3.0, b * 2, a * 1)) Ok((d * 4.0, c * 3.0, b * 2, a))
} }
fn long_f( fn long_f(
@ -313,7 +313,7 @@ fn static_host_function_with_env(config: crate::Config) -> anyhow::Result<()> {
assert_eq!(*guard, 100); assert_eq!(*guard, 100);
*guard = 101; *guard = 101;
(d * 4.0, c * 3.0, b * 2, a * 1) (d * 4.0, c * 3.0, b * 2, a)
} }
fn f_ok( fn f_ok(
@ -327,7 +327,7 @@ fn static_host_function_with_env(config: crate::Config) -> anyhow::Result<()> {
assert_eq!(*guard, 100); assert_eq!(*guard, 100);
*guard = 101; *guard = 101;
Ok((d * 4.0, c * 3.0, b * 2, a * 1)) Ok((d * 4.0, c * 3.0, b * 2, a))
} }
#[derive(Clone)] #[derive(Clone)]
@ -401,7 +401,7 @@ fn dynamic_host_function_without_env(config: crate::Config) -> anyhow::Result<()
Value::F64(values[3].unwrap_f64() * 4.0), Value::F64(values[3].unwrap_f64() * 4.0),
Value::F32(values[2].unwrap_f32() * 3.0), Value::F32(values[2].unwrap_f32() * 3.0),
Value::I64(values[1].unwrap_i64() * 2), Value::I64(values[1].unwrap_i64() * 2),
Value::I32(values[0].unwrap_i32() * 1), Value::I32(values[0].unwrap_i32()),
]) ])
}, },
); );
@ -457,7 +457,7 @@ fn dynamic_host_function_with_env(config: crate::Config) -> anyhow::Result<()> {
Value::F64(values[3].unwrap_f64() * 4.0), Value::F64(values[3].unwrap_f64() * 4.0),
Value::F32(values[2].unwrap_f32() * 3.0), Value::F32(values[2].unwrap_f32() * 3.0),
Value::I64(values[1].unwrap_i64() * 2), Value::I64(values[1].unwrap_i64() * 2),
Value::I32(values[0].unwrap_i32() * 1), Value::I32(values[0].unwrap_i32()),
]) ])
}, },
); );

View File

@ -16,6 +16,7 @@ serde = { version = "1.0.147", features = ["derive"] }
insta = { version = "1.21.1", features = ["json"] } insta = { version = "1.21.1", features = ["json"] }
md5 = "0.7.0" md5 = "0.7.0"
hex = "0.4.3" hex = "0.4.3"
pretty_assertions = "1.3.0"
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"

View File

@ -0,0 +1,365 @@
use anyhow::bail;
use std::path::{Path, PathBuf};
use std::process::Command;
use wasmer_integration_tests_cli::get_wasmer_path;
#[test]
fn wasmer_config_multiget() -> anyhow::Result<()> {
let bin_path = Path::new(env!("WASMER_DIR")).join("bin");
let include_path = Path::new(env!("WASMER_DIR")).join("include");
let bin = format!("{}", bin_path.display());
let include = format!("-I{}", include_path.display());
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("--bindir")
.arg("--cflags")
.output()?;
let lines = String::from_utf8_lossy(&output.stdout)
.lines()
.map(|s| s.trim().to_string())
.collect::<Vec<_>>();
let expected = vec![bin, include];
assert_eq!(lines, expected);
Ok(())
}
#[test]
fn wasmer_config_error() -> anyhow::Result<()> {
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("--bindir")
.arg("--cflags")
.arg("--pkg-config")
.output()?;
let lines = String::from_utf8_lossy(&output.stderr)
.lines()
.map(|s| s.trim().to_string())
.collect::<Vec<_>>();
let expected = vec![
"error: The argument '--bindir' cannot be used with '--pkg-config'",
"",
"USAGE:",
"wasmer config --bindir --cflags",
"",
"For more information try --help",
];
assert_eq!(lines, expected);
Ok(())
}
#[test]
fn config_works() -> anyhow::Result<()> {
let bindir = Command::new(get_wasmer_path())
.arg("config")
.arg("--bindir")
.output()?;
let bin_path = Path::new(env!("WASMER_DIR")).join("bin");
assert_eq!(
String::from_utf8(bindir.stdout).unwrap(),
format!("{}\n", bin_path.display())
);
let bindir = Command::new(get_wasmer_path())
.arg("config")
.arg("--cflags")
.output()?;
let include_path = Path::new(env!("WASMER_DIR")).join("include");
assert_eq!(
String::from_utf8(bindir.stdout).unwrap(),
format!("-I{}\n", include_path.display())
);
let bindir = Command::new(get_wasmer_path())
.arg("config")
.arg("--includedir")
.output()?;
let include_path = Path::new(env!("WASMER_DIR")).join("include");
assert_eq!(
String::from_utf8(bindir.stdout).unwrap(),
format!("{}\n", include_path.display())
);
let bindir = Command::new(get_wasmer_path())
.arg("config")
.arg("--libdir")
.output()?;
let lib_path = Path::new(env!("WASMER_DIR")).join("lib");
assert_eq!(
String::from_utf8(bindir.stdout).unwrap(),
format!("{}\n", lib_path.display())
);
let bindir = Command::new(get_wasmer_path())
.arg("config")
.arg("--libs")
.output()?;
let lib_path = Path::new(env!("WASMER_DIR")).join("lib");
assert_eq!(
String::from_utf8(bindir.stdout).unwrap(),
format!("-L{} -lwasmer\n", lib_path.display())
);
let bindir = Command::new(get_wasmer_path())
.arg("config")
.arg("--prefix")
.output()?;
let wasmer_dir = Path::new(env!("WASMER_DIR"));
assert_eq!(
String::from_utf8(bindir.stdout).unwrap(),
format!("{}\n", wasmer_dir.display())
);
let bindir = Command::new(get_wasmer_path())
.arg("config")
.arg("--pkg-config")
.output()?;
let bin_path = format!("{}", bin_path.display());
let include_path = format!("{}", include_path.display());
let lib_path = format!("{}", lib_path.display());
let wasmer_dir = format!("{}", wasmer_dir.display());
let args = vec![
format!("prefix={wasmer_dir}"),
format!("exec_prefix={bin_path}"),
format!("includedir={include_path}"),
format!("libdir={lib_path}"),
format!(""),
format!("Name: wasmer"),
format!("Description: The Wasmer library for running WebAssembly"),
format!("Version: {}", env!("CARGO_PKG_VERSION")),
format!("Cflags: -I{include_path}"),
format!("Libs: -L{lib_path} -lwasmer"),
];
let lines = String::from_utf8(bindir.stdout)
.unwrap()
.lines()
.map(|s| s.trim().to_string())
.collect::<Vec<_>>();
assert_eq!(lines, args);
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("--config-path")
.output()?;
let config_path = Path::new(env!("WASMER_DIR")).join("wasmer.toml");
assert_eq!(
String::from_utf8_lossy(&output.stdout),
format!("{}\n", config_path.display())
);
// ---- config get
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("get")
.arg("registry.token")
.output()?;
let original_token = String::from_utf8_lossy(&output.stdout);
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("set")
.arg("registry.token")
.arg("abc123")
.output()?;
assert_eq!(String::from_utf8_lossy(&output.stdout), "".to_string());
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("get")
.arg("registry.token")
.output()?;
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"abc123\n".to_string()
);
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("set")
.arg("registry.token")
.arg(original_token.to_string().trim())
.output()?;
assert_eq!(String::from_utf8_lossy(&output.stdout), "".to_string());
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("get")
.arg("registry.token")
.output()?;
assert_eq!(
String::from_utf8_lossy(&output.stdout),
format!("{}\n", original_token.to_string().trim().to_string())
);
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("get")
.arg("registry.url")
.output()?;
let original_url = String::from_utf8_lossy(&output.stdout);
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("set")
.arg("registry.url")
.arg("wapm.dev")
.output()?;
let output_str = String::from_utf8_lossy(&output.stdout);
assert_eq!(output_str, "".to_string());
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("get")
.arg("registry.url")
.output()?;
let output_str = String::from_utf8_lossy(&output.stdout);
assert_eq!(
output_str,
"https://registry.wapm.dev/graphql\n".to_string()
);
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("set")
.arg("registry.url")
.arg(original_url.to_string().trim())
.output()?;
let output_str = String::from_utf8_lossy(&output.stdout);
assert_eq!(output_str, "".to_string());
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("get")
.arg("registry.url")
.output()?;
let output_str = String::from_utf8_lossy(&output.stdout);
assert_eq!(output_str, original_url.to_string());
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("get")
.arg("telemetry.enabled")
.output()?;
let original_output = String::from_utf8_lossy(&output.stdout);
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("set")
.arg("telemetry.enabled")
.arg("true")
.output()?;
assert_eq!(String::from_utf8_lossy(&output.stdout), "".to_string());
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("get")
.arg("telemetry.enabled")
.output()?;
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"true\n".to_string()
);
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("set")
.arg("telemetry.enabled")
.arg(original_output.to_string().trim())
.output()?;
assert_eq!(String::from_utf8_lossy(&output.stdout), "".to_string());
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("get")
.arg("telemetry.enabled")
.output()?;
assert_eq!(
String::from_utf8_lossy(&output.stdout),
original_output.to_string()
);
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("get")
.arg("update-notifications.enabled")
.output()?;
let original_output = String::from_utf8_lossy(&output.stdout);
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("set")
.arg("update-notifications.enabled")
.arg("true")
.output()?;
assert_eq!(String::from_utf8_lossy(&output.stdout), "".to_string());
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("get")
.arg("update-notifications.enabled")
.output()?;
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"true\n".to_string()
);
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("set")
.arg("update-notifications.enabled")
.arg(original_output.to_string().trim())
.output()?;
assert_eq!(String::from_utf8_lossy(&output.stdout), "".to_string());
let output = Command::new(get_wasmer_path())
.arg("config")
.arg("get")
.arg("update-notifications.enabled")
.output()?;
assert_eq!(
String::from_utf8_lossy(&output.stdout),
original_output.to_string()
);
Ok(())
}

View File

@ -277,7 +277,7 @@ fn create_obj(args: Vec<&'static str>, keyword_needle: &str, keyword: &str) -> a
let object_path = operating_dir.join("wasm.obj"); let object_path = operating_dir.join("wasm.obj");
let output: Vec<u8> = WasmerCreateObj { let output: Vec<u8> = WasmerCreateObj {
current_dir: operating_dir.clone(), current_dir: operating_dir,
wasm_path, wasm_path,
output_object_path: object_path.clone(), output_object_path: object_path.clone(),
compiler: Compiler::Cranelift, compiler: Compiler::Cranelift,
@ -292,7 +292,7 @@ fn create_obj(args: Vec<&'static str>, keyword_needle: &str, keyword: &str) -> a
"create-obj successfully completed but object output file `{}` missing", "create-obj successfully completed but object output file `{}` missing",
object_path.display() object_path.display()
); );
let mut object_header_path = object_path.clone(); let mut object_header_path = object_path;
object_header_path.set_extension("h"); object_header_path.set_extension("h");
assert!( assert!(
object_header_path.exists(), object_header_path.exists(),

View File

@ -0,0 +1,20 @@
[package]
name = 'ciuser/testfirstproject'
version = '0.1.0'
description = 'Description for package testfirstproject'
# See more keys and definitions at https://docs.wasmer.io/ecosystem/wapm/manifest
[dependencies]
[[module]]
name = 'testfirstproject'
source = 'testfirstproject.wasm'
abi = 'wasi'
[module.interfaces]
wasi = '0.1.0-unstable'
[[command]]
name = 'testfirstproject'
module = 'testfirstproject'

View File

@ -0,0 +1,6 @@
[package]
name = "wasmer"
version = "0.1.0"
edition = "2021"
[dependencies]

View File

@ -0,0 +1,20 @@
[package]
name = 'ciuser/wasmer'
version = '0.1.0'
description = 'Description for package wasmer'
# See more keys and definitions at https://docs.wasmer.io/ecosystem/wapm/manifest
[dependencies]
[[module]]
name = 'wasmer'
source = 'target/wasm32-wasi/release/wasmer.wasm'
abi = 'wasi'
[module.interfaces]
wasi = '0.1.0-unstable'
[[command]]
name = 'wasmer'
module = 'wasmer'

View File

@ -0,0 +1,13 @@
[package]
name = "WAPMUSERNAME/largewasmfile"
version = "RANDOMVERSION1.RANDOMVERSION2.RANDOMVERSION3"
description = "published from wasmer largewasmfile"
[[module]]
name = "largewasmfile"
source = "largewasmfile.wasm"
abi = "wasi"
[[command]]
name = "largewasmfile"
module = "largewasmfile"

View File

@ -0,0 +1,127 @@
use anyhow::bail;
use std::process::{Command, Stdio};
use wasmer_integration_tests_cli::get_wasmer_path;
macro_rules! check_output {
($output:expr) => {
let stdout_output = std::str::from_utf8(&$output.stdout).unwrap();
let stderr_output = std::str::from_utf8(&$output.stdout).unwrap();
if !$output.status.success() {
bail!("wasmer init failed with: stdout: {stdout_output}\n\nstderr: {stderr_output}");
}
};
}
// Test that wasmer init without arguments works
#[test]
fn wasmer_init_works_1() -> anyhow::Result<()> {
let tempdir = tempfile::tempdir()?;
let path = tempdir.path();
let path = path.join("testfirstproject");
std::fs::create_dir_all(&path)?;
if std::env::var("GITHUB_TOKEN").is_err() {
return Ok(());
}
let wapm_dev_token = std::env::var("WAPM_DEV_TOKEN").ok();
println!("wapm dev token ok...");
if let Some(token) = wapm_dev_token {
let output = Command::new(get_wasmer_path())
.arg("login")
.arg("--registry")
.arg("wapm.dev")
.arg(token)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.stdin(Stdio::null())
.output()?;
check_output!(output);
}
println!("wasmer login ok!");
let output = Command::new(get_wasmer_path())
.arg("init")
.current_dir(&path)
.output()?;
check_output!(output);
let read = std::fs::read_to_string(path.join("wasmer.toml"))
.unwrap()
.lines()
.collect::<Vec<_>>()
.join("\n");
let target = include_str!("./fixtures/init1.toml")
.lines()
.collect::<Vec<_>>()
.join("\n");
pretty_assertions::assert_eq!(read.trim(), target.trim());
Ok(())
}
#[test]
fn wasmer_init_works_2() -> anyhow::Result<()> {
let tempdir = tempfile::tempdir()?;
let path = tempdir.path();
let path = path.join("testfirstproject");
std::fs::create_dir_all(&path)?;
std::fs::write(
path.join("Cargo.toml"),
include_bytes!("./fixtures/init2.toml"),
)?;
std::fs::create_dir_all(path.join("src"))?;
std::fs::write(path.join("src").join("main.rs"), b"fn main() { }")?;
if std::env::var("GITHUB_TOKEN").is_err() {
return Ok(());
}
let wapm_dev_token = std::env::var("WAPM_DEV_TOKEN").ok();
println!("wapm dev token ok...");
if let Some(token) = wapm_dev_token.as_ref() {
let mut cmd = Command::new(get_wasmer_path());
cmd.arg("login");
cmd.arg("--registry");
cmd.arg("wapm.dev");
cmd.arg(token);
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());
cmd.stdin(Stdio::null());
let output = cmd.output()?;
check_output!(output);
}
println!("wasmer login ok!");
let output = Command::new(get_wasmer_path())
.arg("init")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.current_dir(&path)
.output()?;
check_output!(output);
pretty_assertions::assert_eq!(
std::fs::read_to_string(path.join("Cargo.toml")).unwrap(),
include_str!("./fixtures/init2.toml")
);
println!("ok 1");
let read = std::fs::read_to_string(path.join("wasmer.toml"))
.unwrap()
.lines()
.collect::<Vec<_>>()
.join("\n");
let target = include_str!("./fixtures/init4.toml")
.lines()
.collect::<Vec<_>>()
.join("\n");
pretty_assertions::assert_eq!(read.trim(), target.trim());
Ok(())
}

View File

@ -1,7 +1,7 @@
use anyhow::bail; use anyhow::bail;
use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use wasmer_integration_tests_cli::{get_repo_root_path, get_wasmer_path, ASSET_PATH, C_ASSET_PATH}; use wasmer_integration_tests_cli::get_wasmer_path;
#[test] #[test]
fn login_works() -> anyhow::Result<()> { fn login_works() -> anyhow::Result<()> {
@ -11,6 +11,10 @@ fn login_works() -> anyhow::Result<()> {
return Ok(()); return Ok(());
} }
let wapm_dev_token = std::env::var("WAPM_DEV_TOKEN").expect("WAPM_DEV_TOKEN env var not set"); let wapm_dev_token = std::env::var("WAPM_DEV_TOKEN").expect("WAPM_DEV_TOKEN env var not set");
// Special case: GitHub secrets aren't visible to outside collaborators
if wapm_dev_token.is_empty() {
return Ok(());
}
let output = Command::new(get_wasmer_path()) let output = Command::new(get_wasmer_path())
.arg("login") .arg("login")
.arg("--registry") .arg("--registry")

View File

@ -0,0 +1,146 @@
use std::process::Stdio;
use wasmer_integration_tests_cli::{get_wasmer_path, C_ASSET_PATH};
fn create_exe_test_wasm_path() -> String {
format!("{}/{}", C_ASSET_PATH, "qjs.wasm")
}
#[test]
fn wasmer_publish() -> anyhow::Result<()> {
// Only run this test in the CI
if std::env::var("GITHUB_TOKEN").is_err() {
return Ok(());
}
let wapm_dev_token = std::env::var("WAPM_DEV_TOKEN").ok();
let tempdir = tempfile::tempdir()?;
let path = tempdir.path();
let username = "ciuser";
let random1 = format!("{}", rand::random::<u32>());
let random2 = format!("{}", rand::random::<u32>());
let random3 = format!("{}", rand::random::<u32>());
std::fs::copy(create_exe_test_wasm_path(), path.join("largewasmfile.wasm")).unwrap();
std::fs::write(
path.join("wasmer.toml"),
include_str!("./fixtures/init6.toml")
.replace("WAPMUSERNAME", username) // <-- TODO!
.replace("RANDOMVERSION1", &random1)
.replace("RANDOMVERSION2", &random2)
.replace("RANDOMVERSION3", &random3),
)?;
let mut cmd = std::process::Command::new(get_wasmer_path());
cmd.arg("publish");
cmd.arg("--quiet");
cmd.arg("--registry");
cmd.arg("wapm.dev");
cmd.arg(path);
if let Some(token) = wapm_dev_token {
cmd.arg("--token");
cmd.arg(token);
}
let output = cmd.stdin(Stdio::null()).output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(stdout, format!("Successfully published package `{username}/largewasmfile@{random1}.{random2}.{random3}`\n"), "failed to publish: {cmd:?}: {stderr}");
println!("wasmer publish ok! test done.");
Ok(())
}
// Runs a full integration test to test that the flow wasmer init - cargo build -
// wasmer publish is working
#[test]
fn wasmer_init_publish() -> anyhow::Result<()> {
// Only run this test in the CI
if std::env::var("GITHUB_TOKEN").is_err() {
return Ok(());
}
let wapm_dev_token = std::env::var("WAPM_DEV_TOKEN").ok();
let tempdir = tempfile::tempdir()?;
let path = tempdir.path();
let username = "ciuser";
let random1 = format!("{}", rand::random::<u32>());
let random2 = format!("{}", rand::random::<u32>());
let random3 = format!("{}", rand::random::<u32>());
let mut cmd = std::process::Command::new("cargo");
cmd.arg("init");
cmd.arg("--bin");
cmd.arg(path.join("randomversion"));
let _ = cmd
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.unwrap();
let mut cmd = std::process::Command::new("cargo");
cmd.arg("build");
cmd.arg("--release");
cmd.arg("--target");
cmd.arg("wasm32-wasi");
cmd.arg("--manifest-path");
cmd.arg(path.join("randomversion").join("Cargo.toml"));
let _ = cmd
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.unwrap();
// generate the wasmer.toml
let mut cmd = std::process::Command::new(get_wasmer_path());
cmd.arg("init");
cmd.arg("--namespace");
cmd.arg(username);
cmd.arg("--version");
cmd.arg(format!("{random1}.{random2}.{random3}"));
cmd.arg(path.join("randomversion"));
let _ = cmd
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.unwrap();
let s = std::fs::read_to_string(path.join("randomversion").join("wasmer.toml")).unwrap();
println!("{s}");
// publish
let mut cmd = std::process::Command::new(get_wasmer_path());
cmd.arg("publish");
cmd.arg("--quiet");
cmd.arg("--registry");
cmd.arg("wapm.dev");
cmd.arg(path.join("randomversion"));
if let Some(token) = wapm_dev_token {
cmd.arg("--token");
cmd.arg(token);
}
let output = cmd.stdin(Stdio::null()).output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(stdout, format!("Successfully published package `{username}/randomversion@{random1}.{random2}.{random3}`\n"), "failed to publish: {cmd:?}: {stderr}");
println!("wasmer init publish ok! test done.");
Ok(())
}

View File

@ -112,6 +112,10 @@ fn run_whoami_works() -> anyhow::Result<()> {
} }
let ciuser_token = std::env::var("WAPM_DEV_TOKEN").expect("no CIUSER / WAPM_DEV_TOKEN token"); let ciuser_token = std::env::var("WAPM_DEV_TOKEN").expect("no CIUSER / WAPM_DEV_TOKEN token");
// Special case: GitHub secrets aren't visible to outside collaborators
if ciuser_token.is_empty() {
return Ok(());
}
let output = Command::new(get_wasmer_path()) let output = Command::new(get_wasmer_path())
.arg("login") .arg("login")
@ -352,12 +356,12 @@ fn test_wasmer_run_works_with_dir() -> anyhow::Result<()> {
std::fs::copy(wasi_test_wasm_path(), &qjs_path)?; std::fs::copy(wasi_test_wasm_path(), &qjs_path)?;
std::fs::copy( std::fs::copy(
format!("{}/{}", C_ASSET_PATH, "qjs-wapm.toml"), format!("{}/{}", C_ASSET_PATH, "qjs-wasmer.toml"),
temp_dir.path().join("wapm.toml"), temp_dir.path().join("wasmer.toml"),
)?; )?;
assert!(temp_dir.path().exists()); assert!(temp_dir.path().exists());
assert!(temp_dir.path().join("wapm.toml").exists()); assert!(temp_dir.path().join("wasmer.toml").exists());
assert!(temp_dir.path().join("qjs.wasm").exists()); assert!(temp_dir.path().join("qjs.wasm").exists());
// test with "wasmer qjs.wasm" // test with "wasmer qjs.wasm"

View File

@ -40,9 +40,8 @@ mod tests {
*/ */
let command_success = command.status.success(); let command_success = command.status.success();
let test_success = !stderr.contains("** TEST FAILED **"); let test_success = !stderr.contains("** TEST FAILED **");
let success = command_success && test_success;
success command_success && test_success
} }
fn remove_existing_artificats() -> Output { fn remove_existing_artificats() -> Output {