前言

在B站看到一个项目


仅用几十KB就构建了一个Minecraft服务端,运行更是只需要少得可怜的资源(esp32-c3都能运行),于是记录一下在Alpine Linux上的移植过程。

开始

因为使用的是 Alpine Linux,它底层使用 musl 而不是通常的 glibc (Ubuntu) 或 uClibc (Buildroot)。这需要一个专门针对 arm-linux-musleabihf 的交叉编译器,否则编译出的程序在板子上运行时会报错(例如 File not found 或段错误)。
因此我们需要获取 musl 交叉编译工具链,不过我建议在宿主机手动下载然后传到wsl,不然这速度太慢了。

# 下载工具链
wget https://musl.cc/arm-linux-musleabihf-cross.tgz

# 解压
tar -xf arm-linux-musleabihf-cross.tgz

echo 'export PATH=$PATH:/home/luckfox/Desktop/arm-linux-musleabihf-cross/bin' >> ~/.bashrc
source ~/.bashrc
arm-linux-musleabihf-gcc --version

搭建编译环境,运行setup_env.sh

#!/bin/bash

# Setup script for BareIron development environment on WSL/Ubuntu
# Installs Java 21 and Node.js

set -e

echo "[1/3] Updating package lists..."
sudo apt update

echo "[2/3] Installing dependencies (curl, wget, tar, build-essential)..."
sudo apt install -y curl wget tar sudo build-essential

echo "[3/3] Installing Java 21..."
# Check if OpenJDK 21 is available in default repos
if apt-cache search openjdk-21-jdk | grep -q openjdk-21-jdk; then
    sudo apt install -y openjdk-21-jdk
else
    echo "OpenJDK 21 not found in default repositories. Adding PPA..."
    sudo add-apt-repository -y ppa:openjdk-r/ppa
    sudo apt update
    sudo apt install -y openjdk-21-jdk
fi

# Verify Java installation
java -version

echo "Installing Node.js (Latest LTS)..."
# Using NodeSource for up-to-date Node.js
if ! command -v node &> /dev/null; then
    curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
    sudo apt install -y nodejs
else
    echo "Node.js is already installed."
fi

# Verify Node installation
node -v

echo "--------------------------------------------------------"
echo "Setup Complete!"
echo "--------------------------------------------------------"
echo "1. Java 21 and Node.js have been installed."
echo ""
echo "Note: You need to manually install or configure your cross-compilation toolchain."
echo "For Luckfox Pico (Alpine), ensure 'arm-linux-musleabihf-gcc' is in your PATH."
echo ""
echo "Then you can proceed with:"
echo "    ./extract_registries.sh"
echo "    ./build_luckfox.sh --type alpine"

准备注册表数据:运行 ./extract_registries.sh 。(没权限自己添加),手动下载mc 1.21.8 server.jar,放到notchian文件夹,然后继续执行./extract_registries.sh
添加交叉编译编译脚本build_luckfox.sh

#!/bin/bash

# Build script for Luckfox Pico (RV1103)
# Target Architecture: ARMv7l (Cortex-A7)

set -e

# Default settings
BUILD_TYPE="buildroot" # Options: buildroot, ubuntu, alpine
OUTPUT_NAME="bareiron_luckfox"

# Check for registries
if [ ! -f "include/registries.h" ]; then
  echo "Error: 'include/registries.h' is missing."
  echo "Please run './extract_registries.sh' first (requires Minecraft server jar and Java 21+)."
  exit 1
fi

# Help function
print_usage() {
    echo "Usage: $0 [options]"
    echo "Options:"
    echo "  --type <type>    Target system type: 'buildroot' (default), 'ubuntu', or 'alpine'"
    echo "  --help           Show this help message"
}

# Parse arguments
while [[ "$#" -gt 0 ]]; do
    case $1 in
        --type) BUILD_TYPE="$2"; shift ;;
        --help) print_usage; exit 0 ;;
        *) echo "Unknown parameter: $1"; print_usage; exit 1 ;;
    esac
    shift
done

echo "Building for Luckfox Pico ($BUILD_TYPE)..."

# Set compiler based on type
if [ "$BUILD_TYPE" == "buildroot" ]; then
    # Toolchain for Buildroot (uClibc)
    DEFAULT_CC_PATH="/home/buildroot/luckfox-pico/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc"
    CC="${CROSS_COMPILE:-$DEFAULT_CC_PATH}"
    
    if ! command -v "$CC" &> /dev/null && [ ! -x "$CC" ]; then
        echo "Error: Compiler not found at '$CC'"
        echo "Please set CROSS_COMPILE environment variable to your toolchain path."
        exit 1
    fi

elif [ "$BUILD_TYPE" == "ubuntu" ]; then
    # Toolchain for Ubuntu (glibc)
    DEFAULT_CC_PATH="arm-none-linux-gnueabihf-gcc"
    CC="${CROSS_COMPILE:-$DEFAULT_CC_PATH}"
    
    if ! command -v "$CC" &> /dev/null; then
        echo "Error: Compiler '$CC' not found in PATH."
        echo "Please install 'gcc-arm-linux-gnueabihf' or set CROSS_COMPILE."
        exit 1
    fi

elif [ "$BUILD_TYPE" == "alpine" ]; then
    # Toolchain for Alpine (musl)
    # User needs to download a musl toolchain, e.g., from musl.cc or bootlin
    DEFAULT_CC_PATH="arm-linux-musleabihf-gcc"
    CC="${CROSS_COMPILE:-$DEFAULT_CC_PATH}"
    
    # Try to find compiler in common locations if not in PATH
    if ! command -v "$CC" &> /dev/null; then
        # Check user-provided path from common locations
        POTENTIAL_PATH="/home/luckfox/Desktop/arm-linux-musleabihf-cross/bin/$DEFAULT_CC_PATH"
        if [ -x "$POTENTIAL_PATH" ]; then
             CC="$POTENTIAL_PATH"
        fi
    fi

    if ! command -v "$CC" &> /dev/null && [ ! -x "$CC" ]; then
        echo "Error: Compiler '$CC' not found in PATH."
        echo "For Alpine, you need a musl-based toolchain (e.g., arm-linux-musleabihf-gcc)."
        echo "Please download one (e.g. from musl.cc) and add it to PATH or set CROSS_COMPILE."
        exit 1
    fi

else
    echo "Error: Invalid build type '$BUILD_TYPE'. Use 'buildroot', 'ubuntu', or 'alpine'."
    exit 1
fi

echo "Using compiler: $CC"

# Clean previous build
rm -f "$OUTPUT_NAME"

# Compile flags
# -O2: Optimization
# -Iinclude: Include directory
# -lpthread: Threading support (needed for glibc/uclibc, usually implicit in musl but harmless)
# -lm: Math library
# -static: For Alpine/Musl, static linking is often preferred for portability, 
#          but if you have the libraries on the board, dynamic is fine.
#          We will default to dynamic to match other builds, but you can uncomment -static below if needed.
# LDFLAGS="-static" 

$CC src/*.c -O2 -Iinclude -o "$OUTPUT_NAME" -lpthread -lm

if [ -f "$OUTPUT_NAME" ]; then
    echo "Build successful! Binary: $OUTPUT_NAME"
    file "$OUTPUT_NAME"
else
    echo "Build failed."
    exit 1
fi

运行前修改项目中include/globals.h,在嵌入式系统(如 ESP32)上, task_yield() 通常用于让出 CPU 时间片给其他任务,但在 Linux/PC 上,如果这个宏定义为空(或者只是一个空操作),程序就会在一个死循环里全速空转,导致单核 CPU 占用率飙升到 100%。
Linux/Alpine : 使用 usleep(1000) (休眠 1 毫秒)

#ifndef H_GLOBALS
#define H_GLOBALS

#include <stdint.h>
#include <unistd.h>

#ifdef ESP_PLATFORM
  #define WIFI_SSID "your-ssid"
  #define WIFI_PASS "your-password"
  void task_yield ();
#else
  #ifdef _WIN32
    #define task_yield() Sleep(1)
  #else
    #define task_yield() usleep(1000)
  #endif
#endif

#define true 1
#define false 0

// TCP port, Minecraft's default is 25565
#define PORT 25565

// How many players to keep in memory, NOT the amount of concurrent players
// Even when offline, players who have logged on before take up a slot
#define MAX_PLAYERS 16

// How many mobs to allocate memory for
#define MAX_MOBS (MAX_PLAYERS)

// Manhattan distance at which mobs despawn
#define MOB_DESPAWN_DISTANCE 256

// Server game mode: 0 - survival; 1 - creative; 2 - adventure; 3 - spectator
#define GAMEMODE 0

// Max render distance, determines how many chunks to send
#define VIEW_DISTANCE 2

// Time between server ticks in microseconds (default = 1s)
#define TIME_BETWEEN_TICKS 1000000

// Calculated from TIME_BETWEEN_TICKS
#define TICKS_PER_SECOND ((float)1000000 / TIME_BETWEEN_TICKS)

// Initial world generation seed, will be hashed on startup
// Used in generating terrain and biomes
#define INITIAL_WORLD_SEED 0xA103DE6C

// Initial general RNG seed, will be hashed on startup
// Used in random game events like item drops and mob behavior
#define INITIAL_RNG_SEED 0xE2B9419

// Size of each bilinearly interpolated area ("minichunk")
// For best performance, CHUNK_SIZE should be a power of 2
#define CHUNK_SIZE 8

// Terrain low point - should start a bit below sea level for rivers/lakes
#define TERRAIN_BASE_HEIGHT 60

// Cave generation Y level
#define CAVE_BASE_DEPTH 24

// Size of every major biome in multiples of CHUNK_SIZE
// For best performance, should also be a power of 2
#define BIOME_SIZE (CHUNK_SIZE * 8)

// Calculated from BIOME_SIZE
#define BIOME_RADIUS (BIOME_SIZE / 2)

// How many visited chunk coordinates to "remember"
// The server will not re-send chunks that the player has recently been in
// Must be at least 1, otherwise chunks will be sent on each position update
#define VISITED_HISTORY 4

// How many player-made block changes to allow
// Determines the fixed amount of memory allocated to blocks
#define MAX_BLOCK_CHANGES 20000

// If defined, writes and reads world data to/from disk (or flash).
// This is a synchronous operation, and can cause performance issues if
// frequent random disk access is slow. Data is still stored in and
// accessed from memory - reading from disk is only done on startup.
// When targeting ESP-IDF, LittleFS is used to manage flash reads and
// writes. Flash is typically *very* slow and unreliable, which is why
// this option is disabled by default when targeting ESP-IDF.
#ifndef ESP_PLATFORM
  #define SYNC_WORLD_TO_DISK
#endif

// The minimum interval (in microseconds) at which certain data is written
// to disk/flash. Bounded on the low end by TIME_BETWEEN_TICKS. By default,
// applies only to player data. Block changes are written as soon as they
// are made, but in much smaller portions. Set DISK_SYNC_BLOCKS_ON_INTERVAL
// to make this apply to block changes as well.
#define DISK_SYNC_INTERVAL 15000000

// Whether to sync block changes to disk on an interval, instead of syncing
// on each change. On systems with fast random disk access, this shouldn't
// be necessary.
// #define DISK_SYNC_BLOCKS_ON_INTERVAL

// Time in microseconds to spend waiting for data transmission before
// timing out. Default is 15s, which leaves 5s to prevent starving other
// clients from Keep Alive packets.
#define NETWORK_TIMEOUT_TIME 15000000

// Size of the receive buffer for incoming string data
#define MAX_RECV_BUF_LEN 256

// If defined, sends the server brand to clients. Doesn't do much, but will
// show up in the top-left of the F3/debug menu, in the Minecraft client.
// You can change the brand string in the "brand" variable in src/globals.c
#define SEND_BRAND

// If defined, rebroadcasts ALL incoming movement updates, disconnecting
// movement from the server's tickrate. This makes movement much smoother
// on very low tickrates, at the cost of potential network instability when
// hosting more than just a couple of players. When disabling this on low
// tickrates, consider disabling SCALE_MOVEMENT_UPDATES_TO_PLAYER_COUNT too.
#define BROADCAST_ALL_MOVEMENT

// If defined, scales the frequency at which player movement updates are
// broadcast based on the amount of players, reducing overhead for higher
// player counts. For very many players, makes movement look jittery.
// It is not recommended to use this if BROADCAST_ALL_MOVEMENT is disabled
// on low tickrates, as that might drastically decrease the update rate.
#define SCALE_MOVEMENT_UPDATES_TO_PLAYER_COUNT

// If defined, calculates fluid flow when blocks are updated near fluids
// Somewhat computationally expensive and potentially unstable
#define DO_FLUID_FLOW

// If defined, allows players to craft and use chests.
// Chests take up 15 block change slots each, require additional checks,
// and use some terrible memory hacks to function. On some platforms, this
// could cause bad performance or even crashes during gameplay.
#define ALLOW_CHESTS

// If defined, enables flight for all players. As a side-effect, allows
// players to sprint when starving.
// #define ENABLE_PLAYER_FLIGHT

// If defined, enables the item pickup animation when mining a block/
// Does not affect how item pickups work! Items from broken blocks still
// get placed directly in the inventory, this is just an animation.
// Relatively inexpensive, though requires sending a few more packets
// every time a block is broken.
#define ENABLE_PICKUP_ANIMATION

// If defined, players are able to receive damage from nearby cacti.
#define ENABLE_CACTUS_DAMAGE

// If defined, logs unrecognized packet IDs
// #define DEV_LOG_UNKNOWN_PACKETS

// If defined, logs cases when packet length doesn't match parsed byte count
#define DEV_LOG_LENGTH_DISCREPANCY

// If defined, log chunk generation events
// #define DEV_LOG_CHUNK_GENERATION

// If defined, allows dumping world data by sending 0xBEEF (big-endian),
// and uploading world data by sending 0xFEED, followed by the data buffer.
// Doesn't implement authentication, hence disabled by default.
// #define DEV_ENABLE_BEEF_DUMPS

#define STATE_NONE 0
#define STATE_STATUS 1
#define STATE_LOGIN 2
#define STATE_TRANSFER 3
#define STATE_CONFIGURATION 4
#define STATE_PLAY 5

extern ssize_t recv_count;
extern uint8_t recv_buffer[MAX_RECV_BUF_LEN];

extern uint32_t world_seed;
extern uint32_t rng_seed;

extern uint16_t world_time;
extern uint32_t server_ticks;

extern char motd[];
extern uint8_t motd_len;

#ifdef SEND_BRAND
  extern char brand[];
  extern uint8_t brand_len;
#endif

extern uint16_t client_count;

typedef struct {
  short x;
  short z;
  uint8_t y;
  uint8_t block;
} BlockChange;

#pragma pack(push, 1)

typedef struct {
  uint8_t uuid[16];
  char name[16];
  int client_fd;
  short x;
  uint8_t y;
  short z;
  short visited_x[VISITED_HISTORY];
  short visited_z[VISITED_HISTORY];
  #ifdef SCALE_MOVEMENT_UPDATES_TO_PLAYER_COUNT
    uint16_t packets_since_update;
  #endif
  int8_t yaw;
  int8_t pitch;
  uint8_t grounded_y;
  uint8_t health;
  uint8_t hunger;
  uint16_t saturation;
  uint8_t hotbar;
  uint16_t inventory_items[41];
  uint16_t craft_items[9];
  uint8_t inventory_count[41];
  uint8_t craft_count[9];
  // Usage depends on player's flags, see below
  // When no flags are set, acts as cursor item ID
  uint16_t flagval_16;
  // Usage depends on player's flags, see below
  // When no flags are set, acts as cursor item count
  uint8_t flagval_8;
  // 0x01 - attack cooldown, uses flagval_8 as the timer
  // 0x02 - has not spawned yet
  // 0x04 - sneaking
  // 0x08 - sprinting
  // 0x10 - eating, makes flagval_16 act as eating timer
  // 0x20 - client loading, uses flagval_16 as fallback timer
  // 0x40 - movement update cooldown
  // 0x80 - craft_items lock (for storing pointers)
  uint8_t flags;
} PlayerData;

typedef struct {
  uint8_t type;
  short x;
  // When the mob is dead (health is 0), the Y coordinate acts
  // as a timer for deleting and deallocating the mob
  uint8_t y;
  short z;
  // Lower 5 bits: health
  // Middle 1 bit: sheep sheared, unused for other mobs
  // Upper 2 bits: panic timer
  uint8_t data;
} MobData;

#pragma pack(pop)

union EntityDataValue {
  uint8_t byte;
  int pose;
};

typedef struct {
  uint8_t index;
  // 0 - Byte
  // 21 - Pose
  int type;
  union EntityDataValue value;
} EntityData;

extern BlockChange block_changes[MAX_BLOCK_CHANGES];
extern int block_changes_count;

extern PlayerData player_data[MAX_PLAYERS];
extern int player_data_count;

extern MobData mob_data[MAX_MOBS];

#endif

执行命令

chmod +x build_luckfox.sh
./build_luckfox.sh --type alpine

传输到板子root目录上,配置后台运行

编写openrc/bareiron

#!/sbin/openrc-run

name="BareIron Minecraft Server"
description="Minimalist Minecraft server for embedded systems"

# Path to the executable
command="/root/bareiron_luckfox"

# Background the process
command_background=true

# PID file location
pidfile="/run/${RC_SVCNAME}.pid"

# Working directory (where world.bin is stored)
directory="/root"

# Redirect output to log file (optional, helps debugging)
output_log="/var/log/bareiron.log"
error_log="/var/log/bareiron.err"

depend() {
    # Start after networking is ready
    need net
    # Use DNS if available
    use dns logger
}

start_pre() {
    # Ensure the binary exists and is executable
    if [ ! -x "$command" ]; then
        eerror "Executable not found or not executable: $command"
        return 1
    fi
    einfo "Starting $name..."
}

编写install_service.sh

#!/bin/sh

# Installation script for BareIron OpenRC service on Alpine Linux
# Run this on your Luckfox Pico

SERVICE_NAME="bareiron"
SERVICE_SCRIPT="openrc/bareiron"
TARGET_DIR="/etc/init.d"
BINARY_NAME="bareiron_luckfox"
BINARY_PATH="/root/$BINARY_NAME"

# Check if running as root
if [ "$(id -u)" -ne 0 ]; then
    echo "Error: This script must be run as root."
    exit 1
fi

# Check if binary exists on the board
if [ ! -f "$BINARY_PATH" ]; then
    echo "Warning: Binary '$BINARY_PATH' not found."
    echo "Make sure you have copied '$BINARY_NAME' to /root/"
fi

echo "Installing service script..."
if [ ! -f "$SERVICE_SCRIPT" ]; then
    echo "Error: Service script '$SERVICE_SCRIPT' not found in current directory."
    echo "Please upload the 'openrc' folder to the board first."
    exit 1
fi

cp "$SERVICE_SCRIPT" "$TARGET_DIR/$SERVICE_NAME"
chmod +x "$TARGET_DIR/$SERVICE_NAME"

echo "Service installed to $TARGET_DIR/$SERVICE_NAME"

echo "Adding service to default runlevel (autostart on boot)..."
rc-update add "$SERVICE_NAME" default

echo "---------------------------------------------------"
echo "Installation Complete!"
echo "---------------------------------------------------"
echo "To start the server now:"
echo "  rc-service $SERVICE_NAME start"
echo ""
echo "To stop the server:"
echo "  rc-service $SERVICE_NAME stop"
echo ""
echo "To check status:"
echo "  rc-service $SERVICE_NAME status"
echo ""
echo "To view logs:"
echo "  tail -f /var/log/bareiron.log"

执行命令,然后删除openrc/bareiron和install_service.sh

chmod +x install_service.sh
./install_service.sh
  • 启动: rc-service bareiron start
  • 停止: rc-service bareiron stop
  • 查看日志: tail -f /var/log/bareiron.log

    如何游玩

  • 下载 : 直接搜索 "HMCL 下载" 或访问其 GitHub/官网。
  • 使用方法 :
  • 打开 HMCL。
  • 点击“账户” -> 添加离线账户 (随便起个名字)。
  • 点击“下载” -> 选择 1.21.8 版本(必须与服务器版本一致)。
  • 下载完成后,点击“启动游戏”。
  • 进入游戏后,选择“多人游戏” -> “添加服务器” -> 输入Luckfox 板子的 IP 地址 -> 连接。

    配置内网穿透

    使用樱花frp创建tcp隧道重新配置内外穿透
    配置域名,具体看设置 SRV 解析
    多人服务器,填入域名(一般延迟80ms)
    enjoy yourself!