Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ jobs:
インストール方法は各OSの手順に従ってください。
(ビルド完了後にアセットが追加されます)

## License / Source
- License: GPL-2.0-only(`LICENSE`)
- Third-party notices: `THIRD_PARTY_NOTICES.md`
- Corresponding Source: `opcode-logic-${{ steps.version.outputs.version }}-corresponding-source.tar.gz`

# 2. 全プラットフォームで並列ビルド
build:
needs: create-release
Expand Down Expand Up @@ -128,4 +133,61 @@ jobs:
projectPath: .
tagName: v${{ needs.create-release.outputs.version }}
releaseId: ${{ needs.create-release.outputs.release_id }}
args: ${{ matrix.args }}
args: ${{ matrix.args }}

# 3. 対応ソース(Corresponding Source)と告知ファイルをReleaseに添付
corresponding-source:
needs: create-release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Checkout tag
run: |
git checkout "v${{ needs.create-release.outputs.version }}"

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Vendor Rust crates (cargo vendor)
working-directory: src-tauri
run: |
cargo vendor vendor
mkdir -p .cargo
cat > .cargo/config.toml <<'EOF'
[source.crates-io]
replace-with = "vendored-sources"

[source.vendored-sources]
directory = "vendor"
EOF

- name: Create corresponding source tarball
run: |
VERSION="${{ needs.create-release.outputs.version }}"
NAME="opcode-logic-${VERSION}-corresponding-source"
mkdir -p dist
rsync -a --delete \
--exclude '.git' \
--exclude 'node_modules' \
--exclude 'release' \
--exclude 'dist' \
./ "dist/${NAME}/"
tar -C dist -czf "dist/${NAME}.tar.gz" "${NAME}"
sha256sum "dist/${NAME}.tar.gz" > "dist/${NAME}.tar.gz.sha256"

- name: Upload corresponding source and notices
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.create-release.outputs.version }}
files: |
dist/*.tar.gz
dist/*.sha256
LICENSE
THIRD_PARTY_NOTICES.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
360 changes: 339 additions & 21 deletions LICENSE

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ npm run check

## ライセンス

MIT License
GPL-2.0-only

## 推奨IDE設定

Expand Down
12 changes: 10 additions & 2 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
## Third-party notices

このプロジェクト(OpCode Logic)は **GPL-2.0-only** で配布します。
デスクトップアプリとして **バイナリ配布**(GitHub Releases / 将来はサイト配布)を行う場合、GPLv2(2-only)の条件に従い、受領者が本プロジェクトの対応ソースコードを入手できる状態にします。

- **本プロジェクトのライセンス本文**: `LICENSE`(GPLv2)
- **本プロジェクトの対応ソース**: リポジトリのソース、または配布ページ(Release/サイト)から入手可能にします。

このプロジェクトは以下のサードパーティソフトウェアに依存します。配布形態(静的/動的リンク、同梱物)によっては、それぞれのライセンス条件に従う必要があります。

### Unicorn Engine
- **Project**: Unicorn CPU Emulator
- **Repository**: `https://github.com/unicorn-engine/unicorn`
- **Used via**: Rust crate `unicorn-engine`
- **License**: GPL-2.0(詳細は上記リポジトリのLICENSEを参照)
- **Crate version**: `unicorn-engine 2.1.5`(`src-tauri/Cargo.toml` / `src-tauri/Cargo.lock`)
- **License**: GPL-2.0-only(詳細は上記リポジトリのLICENSEを参照)

### Keystone Engine
- **Project**: Keystone Assembler Engine
- **Repository**: `https://github.com/keystone-engine/keystone`
- **Used via**: Rust crate `keystone-engine`
- **License**: GPL-2.0(詳細は上記リポジトリのLICENSEを参照)
- **Crate version**: `keystone-engine 0.1.0`(`src-tauri/Cargo.toml` / `src-tauri/Cargo.lock`)
- **License**: GPL-2.0-only(詳細は上記リポジトリのLICENSEを参照)

2 changes: 1 addition & 1 deletion package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"test:coverage": "vitest --coverage",
"tauri": "tauri"
},
"license": "MIT",
"license": "GPL-2.0-only",
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2"
Expand Down
39 changes: 39 additions & 0 deletions scripts/test-all-stages.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

failures=0
failed_list=()

shopt -s nullglob
tests=( "$ROOT_DIR"/stages/*/*/*/test.sh "$ROOT_DIR"/stages/*/*/*/*/test.sh )

if [ ${#tests[@]} -eq 0 ]; then
echo "No stage test.sh found under stages/."
exit 1
fi

for t in "${tests[@]}"; do
rel="${t#"$ROOT_DIR"/}"
echo "==> $rel"
if bash "$t"; then
echo "PASS: $rel"
else
echo "FAIL: $rel" >&2
failures=$((failures + 1))
failed_list+=( "$rel" )
fi
echo
done

if [ "$failures" -ne 0 ]; then
echo "FAILED ($failures):" >&2
for f in "${failed_list[@]}"; do
echo " - $f" >&2
done
exit 1
fi

echo "All stage tests passed (${#tests[@]})."

2 changes: 2 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ version = "0.2.0"
description = "Assemble Code learning"
authors = ["saku0512"]
edition = "2021"
license = "GPL-2.0-only"
default-run = "opcode-logic"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

Expand Down
150 changes: 150 additions & 0 deletions src-tauri/src/bin/stage_runner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use opcode_logic_lib::levels;
use opcode_logic_lib::vm::Syntax;
use opcode_logic_lib::x86_runtime;

use std::fs;
use std::path::PathBuf;

fn print_usage_and_exit() -> ! {
eprintln!(
"Usage:\n stage_runner --level-id <ID> --asm <path> [--syntax Intel|Att] [--max-instructions N]\n"
);
std::process::exit(2);
}

fn main() {
let mut level_id: Option<String> = None;
let mut asm_path: Option<PathBuf> = None;
let mut syntax = Syntax::Intel;
let mut max_instructions: usize = 50_000;

let mut args = std::env::args().skip(1);
while let Some(a) = args.next() {
match a.as_str() {
"--level-id" => level_id = args.next(),
"--asm" => asm_path = args.next().map(PathBuf::from),
"--syntax" => {
let s = args.next().unwrap_or_else(|| {
eprintln!("Missing value for --syntax");
print_usage_and_exit();
});
syntax = match s.as_str() {
"Intel" => Syntax::Intel,
"Att" => Syntax::Att,
other => {
eprintln!("Unknown syntax: {}", other);
print_usage_and_exit();
}
};
}
"--max-instructions" => {
let s = args.next().unwrap_or_else(|| {
eprintln!("Missing value for --max-instructions");
print_usage_and_exit();
});
max_instructions = s.parse().unwrap_or_else(|_| {
eprintln!("Invalid number for --max-instructions: {}", s);
print_usage_and_exit();
});
}
"-h" | "--help" => print_usage_and_exit(),
other => {
eprintln!("Unknown arg: {}", other);
print_usage_and_exit();
}
}
}

let level_id = level_id.unwrap_or_else(|| {
eprintln!("Missing --level-id");
print_usage_and_exit();
});
let asm_path = asm_path.unwrap_or_else(|| {
eprintln!("Missing --asm");
print_usage_and_exit();
});

let level = levels::get_level(&level_id).unwrap_or_else(|| {
eprintln!("Unknown level id: {}", level_id);
std::process::exit(2);
});

let code = fs::read_to_string(&asm_path).unwrap_or_else(|e| {
eprintln!("Failed to read asm file {}: {}", asm_path.display(), e);
std::process::exit(2);
});

let mut failures = 0usize;
for (idx, (test_in, expected)) in level.test_cases.iter().enumerate() {
let run =
match x86_runtime::run_x86_64(&code, syntax.clone(), test_in.clone(), max_instructions)
{
Ok(r) => r,
Err(e) => {
eprintln!(
"[{}] FAIL: runtime error for input {:?}: {}",
idx + 1,
test_in,
e
);
failures += 1;
continue;
}
};

let state = run.state;
if let Some(err) = state.error.clone() {
eprintln!(
"[{}] FAIL: vm error for input {:?}: {}",
idx + 1,
test_in,
err
);
failures += 1;
continue;
}

let got = if !state.output.is_empty() {
state.output.clone()
} else {
// Mirror app behavior: treat RAX as output only if no stream output exists.
let rax = *state
.registers
.get(&opcode_logic_lib::vm::Register::RAX)
.unwrap_or(&0);
if state.exited && rax == 60 && expected.len() == 1 && expected[0] != 60 {
vec![]
} else {
vec![rax]
}
};

let ok = if !got.is_empty() {
got.len() >= expected.len() && &got[0..expected.len()] == expected.as_slice()
} else {
expected.is_empty()
};

if ok {
eprintln!("[{}] PASS", idx + 1);
} else {
eprintln!(
"[{}] FAIL: input {:?}\n expected: {:?}\n got: {:?}",
idx + 1,
test_in,
expected,
got
);
failures += 1;
}
}

if failures != 0 {
eprintln!(
"FAILED {} / {} test case(s).",
failures,
level.test_cases.len()
);
std::process::exit(1);
}
}
Loading