elixir 普通应用原生发布

步骤

初始化

会创建 rel 文件夹,带 vm 的配置和环境变量

1
mix release.init

指定 release

如果要生成指定的 release 的名字,则修改 mix.exs

一般不需要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def project do
[
.........
releases: releases()
]
end

defp releases do
[
aaa: [
include_executables_for: [:unix],
applications: [
demo1: :permanent,
demo2: :permanent
]
]
]
end

运行

1
2
3
MIX_ENV=prod mix release
MIX_ENV=prod mix release --overwrite
MIX_ENV=prod mix release aaa

指定 vm 参数

1
2
3
4
export RELEASE_DISTRIBUTION=name
export RELEASE_NODE=aaa@127.0.0.1
export RELEASE_COOKIE=123456
bin/abc start_iex

发布

最终需要的目录

1
_build/prod/rel/$RELEASE_NAME/

elixir 发布之参数配置

位置

1
2
3
4
5
6
$ tree rel
rel
├── env.bat.eex
├── env.sh.eex
├── remote.vm.args.eex
└── vm.args.eex

说明

env.sh.eex

release 脚本启动之前,可以执行任何操作

运行期加载环境变量

1
2
export AAA=<%= System.get_env("ABC") %>
export BBB=12345

测试

1
2
3
4
5
6
mix release
export ABC="asdasd"
_build/dev/rel/demo/bin/demo start_iex
System.get_env("AAA")
System.get_env("ABC")
System.get_env("BBB")

sys.config

改到 config 目录里面去了

vm.args.eex

配置虚拟机参数,只支持编译期加载环境变量

编译期

默认只能编译期加载

1
2
-kernel inet_dist_listen_min <%= System.get_env("DIST_PORT") %>
-kernel inet_dist_listen_max <%= System.get_env("DIST_PORT") %>

测试

1
DIST_PORT=5555 mix release

运行期

运行期加载环境变量为 hack 的方式

env.sh.eex

1
2
3
4
VERSION_DIR="$(ls "$RELEASE_ROOT/releases/" | head -n1)"
export DIST_PORT="${DIST_PORT:-7878}"
sed -i "s/\${DIST_PORT}/${DIST_PORT}/g" \
"$RELEASE_ROOT/releases/$VERSION_DIR/vm.args"

vm.args.eex

1
2
-kernel inet_dist_listen_min ${DIST_PORT}
-kernel inet_dist_listen_max ${DIST_PORT}

测试

1
2
MIX_ENV=prod mix release
DIST_PORT=5555 _build/prod/rel/demo/bin/demo start_iex

elixir 之 protocol

说明

为不同的数据类型定义通用的行为

场景

给一些类型增加功能,而不修改原有模块

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
defmodule DemoStruct1 do
defstruct [:aaa, :bbb]

def new(aaa, bbb) do
%DemoStruct1{aaa: aaa, bbb: bbb}
end
end

defmodule DemoStruct2 do
defstruct [:ccc, :ddd]

def new(ccc, ddd) do
%DemoStruct2{ccc: ccc, ddd: ddd}
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
defprotocol DemoProtocol do
@fallback_to_any true
def xxx(struct_obj, source, dest)
end

defimpl DemoProtocol, for: DemoStruct1 do
require Logger

def xxx(%DemoStruct1{aaa: aaa, bbb: bbb}, source, dest) do
info = "第一个 #{inspect(aaa)} #{inspect(bbb)} #{inspect(source)} #{inspect(dest)}"
Logger.debug(info)
end
end

defimpl DemoProtocol, for: DemoStruct2 do
require Logger

def xxx(%DemoStruct2{ccc: ccc, ddd: ddd}, source, dest) do
info = "另外一个 #{inspect(ccc)} #{inspect(ddd)} #{inspect(source)} #{inspect(dest)}"
Logger.debug(info)
end
end

defimpl DemoProtocol, for: Any do
require Logger

def xxx(object, source, dest) do
info = "any 类型 #{inspect(object)} #{inspect(source)} #{inspect(dest)}"
Logger.debug(info)
end
end
1
2
3
4
5
6
7
8
9
10
11
defmodule Demo do
def demo do
obj1 = DemoStruct1.new(111, 222)
DemoProtocol.xxx(obj1, "aaaaaa", "bbbbbbbb")

obj2 = DemoStruct2.new(333, 444)
DemoProtocol.xxx(obj2, "yyyyyyyyy", "zzzzzzzzzzzz")

DemoProtocol.xxx(111, "11111", "22222")
end
end

elixir 之 behavior

说明

类似面向对象的接口

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
defmodule Demo do
defmodule IDemoBehaviour do
@callback say_hello(name :: String.Chars.t()) :: atom()
@callback say_bye(name :: String.Chars.t()) :: atom()
end
end

defmodule Demo1 do
@behaviour Demo.IDemoBehaviour
require Logger

def say_hello(name) do
Logger.debug("say_hello in demo1 " <> name)
end

def say_bye(name) do
Logger.debug("say_bye in demo1 " <> name)
end
end

defmodule Demo2 do
@behaviour Demo.IDemoBehaviour
require Logger

# @impl true 主要用于静态分析器
# 其修饰的方法必须在 behaviour 里面存在,否则会警告
@impl true
def say_hello(name) do
Logger.debug"say_hello in demo2 " <> name)
end

# 一个方法用 @impl 修饰了,其它的也必须用 @impl 修饰,否则会警告
@impl Demo.IDemoBehaviour
def say_bye(name) do
Logger.debug("say_bye in demo2 " <> name)
end
end

测试

1
2
3
4
5
Demo1.say_hello("root")
Demo1.say_bye("root")

Demo2.say_hello("root")
Demo2.say_bye("root")

elixir 通过函数回调

用法

匿名函数

1
2
3
4
require Logger
sum = &(&1 + &2 + &3 + &4)
result = sum.(1, 2, 3, 4)
Logger.debug(result)

等同于

1
2
3
4
5
6
7
8
Logger.debug

def sum(a, b, c , d) do
a + b + c + d
end

result = sum(1, 2, 3, 4)
Logger.debug(result)

直接调用

&模块名字.方法名/参数个数

1
&__MODULE__.demo_func_name/1

例如

1
2
demo_func = &demo_module_name.demo_func_name/1
demo_func.(123)

官方例子

1
2
3
4
5
6
7
8
9
defmodule Demo do
require Logger

def check_length(text)do
Logger.debug(text)
String.length(text) === 3
end
end
Enum.any?(["foo", "bar", "hello"], &Demo.check_length/1)

apply 法

1
apply(__MODULE__, :demo_func_name, [123])

如果模块名为 string

1
2
module_atom = String.to_existing_atom("Elixir.Demo")
apply(module_atom, :on_data, [data])

debootstrap

说明

用来生成 rootfs

步骤

准备

debian

1
sudo apt install qemu-user-static debootstrap binfmt-support

manjaro

1
sudo pacman -S qemu-user-static debootstrap qemu-user-static-binfmt

创建

1
2
# 用 https 的源
sudo debootstrap --arch armhf --foreign --no-check-gpg bookworm deb_fs https://mirrors.ustc.edu.cn/debian

基础配置

1
2
3
4
sudo cp /usr/bin/qemu-arm-static ./deb_fs/usr/bin/

# chroot 需要手动 mount 一大堆
sudo systemd-nspawn -D ./deb_fs

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sudo bwrap \
--bind ./deb_fs / \
--dev /dev \
--dev-bind /dev/pts /dev/pts \
--dev-bind /dev/null /dev/null \
--dev-bind /dev/zero /dev/zero \
--dev-bind /dev/random /dev/random \
--dev-bind /dev/urandom /dev/urandom \
--proc /proc \
--ro-bind /sys /sys \
--tmpfs /tmp \
--setenv HOME /root \
--setenv PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
/bin/bash
1
/debootstrap/debootstrap --second-stage
1
apt install -y neovim

profile

/etc/profile.d/path.sh

1
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

/etc/profile.d/ls.sh

1
2
3
alias ll='ls -la --color=auto' 2>/dev/null
alias l.='ls --color=auto -d .*' 2>/dev/null
alias ls='ls --color=auto' 2>/dev/null

/etc/profile.d/proxy.sh

1
2
alias pon="export HTTP_PROXY=http://127.0.0.1:8118 HTTPS_PROXY=http://127.0.0.1:8118 ALL_PROXY=socks5://127.0.0.1:1080 NO_PROXY=localhost,127.0.0.1,10.0.2.1"
alias poff="unset HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY"

/etc/profile.d/python.sh

1
2
export PYTHONPYCACHEPREFIX=/dev/null
export UV_INDEX="https://mirrors.aliyun.com/pypi/simple"

~/.bashrc

1
[[ -f /etc/profile ]] && source /etc/profile

必备工具

1
2
source /etc/profile
apt install -y iputils-ping net-tools ncat file

测试

制作镜像

1
2
3
4
5
6
7
8
9
10
11
# dd if=/dev/zero of=rootfs.ext4 bs=1M count=512
dd if=/dev/zero of=rootfs.ext4 bs=1G count=1
sudo mkfs.ext4 rootfs.ext4

mkdir mnt_fs
sudo mount -o loop rootfs.ext4 mnt_fs

sudo cp -rf ./deb_fs/* ./mnt_fs/

sudo umount mnt_fs
rm -rf mnt_fs

qemu

1
2
3
4
5
6
7
8
qemu-system-arm \
-M vexpress-a9 \
-m 512M \
-kernel ~/downloads/linux-5.10.191/output/arch/arm/boot/zImage \
-dtb ~/downloads/linux-5.10.191/output/arch/arm/boot/dts/vexpress-v2p-ca9.dtb \
-nographic \
-append "root=/dev/mmcblk0 rw console=ttyAMA0" \
-sd ~/downloads/rootfs.ext4

buildroot 辅助用法

文件打包

output/target 目录先做好备份

方法

方法 1

1
make O=output menuconfig

设置为 overlay_fs

1
2
System configuration  --->
(overlay_fs) Root filesystem overlay directories

目录结构如下

1
2
3
4
5
overlay_fs
└── usr
└── local
└── bin
└── demo

编译

1
2
3
4
# 这个会处理 output/target 目录,但是不会生成 ext2 文件
make O=output target-finalize -j$(nproc)
# 生成 etx2
make O=output rootfs-ext2 -j$(nproc)

或者

1
2
make O=output -j$(nproc)
make O=output all -j$(nproc)

方法 2

1
make O=output menuconfig

设置为 overlay_fs

1
2
System configuration  --->
(custom_fs/post.sh) Custom scripts to run before creating filesystem images

目录结构如下

1
2
3
4
custom_fs
├── files
│   └── demo
└── post.sh

post.sh

1
2
3
4
5
6
7
8
9
10
#!/bin/sh

# output/target
TARGET_DIR=$1
CURRENT_DIR=$(cd "$(dirname "$0")";pwd)

mkdir -p ${TARGET_DIR}/usr/local/bin
cp -rf ${CURRENT_DIR}/files/* ${TARGET_DIR}/usr/local/bin/

chmod a+x ${TARGET_DIR}/usr/local/bin/*

编译

1
2
3
4
# 这个会处理 output/target 目录,但是不会生成 ext2 文件
make O=output target-finalize -j$(nproc)
# 生成 etx2
make O=output rootfs-ext2 -j$(nproc)

或者

1
2
make O=output -j$(nproc)
make O=output all -j$(nproc)

make 用法

1
2
3
4
5
6
7
8
9
10
11
# uboot
make O=output uboot-dirclean
make O=output uboot-rebuild -j$(nproc)

# 内核
make O=output linux-dirclean
make O=output linux-rebuild -j$(nproc)

# 文件系统
make O=output busybox-dirclean
make O=output busybox-rebuild -j$(nproc)

清理

1
2
3
4
5
6
7
8
# 清理特定包,不删除下载的源码
make O=output xxx-dirclean

# 保留下载的源码
make O=output clean

# 删除下载的源码
make O=output distclean

nfs

配置好网络, tftp, nfs 等服务

配置

内核

启用 nfs,默认已经开启了

1
make O=output linux-menuconfig
1
2
3
4
5
File systems  --->
[*] Network File Systems --->
<*> NFS client support
<*> NFS client support for NFS version 3
[*] Root file system on NFS

uboot

核心已经支持, 就算不选中, nfs 也可以加载

1
make O=output uboot-menuconfig
1
2
Command line interface  --->
[*] nfs

整体

启用 uImage, 设置加载地址

1
make O=output menuconfig
1
2
3
4
Kernel  --->
[*] Linux Kernel
Kernel binary format (uImage)
(0x60010000) load address (for 3.7+ multi-platform image)

打包 rootfs

1
2
Filesystem images  --->
[*] tar the root filesystem

uboot 配置环境变量

1
2
Bootloaders  --->
(uboot.env) Text file with default environment

uboot.env

内存地址一定要注意,否则可能会无法启动

1
2
3
4
5
6
7
ipaddr=10.0.2.222
# ttfp 服务器 ip
serverip=10.0.2.1
# 最后的 ip 不能缺
root=/dev/nfs rw nfsroot=10.0.2.1:/mnt/nfs/rootfs,nfsvers=3 init=/linuxrc console=ttyAMA0 ip=10.0.2.222
# bootm 中间的 - 不能缺
bootcmd=tftp 0x60010000 uImage; tftp 0x61000000 vexpress-v2p-ca9.dtb; bootm 0x60010000 - 0x61000000

编译

1
make O=output -j$(nproc)

测试

1
2
3
4
5
6
7
sudo qemu-system-arm \
-M vexpress-a9 \
-m 512M \
-kernel ~/downloads/buildroot-2025.02.3/output/images/u-boot \
-nographic \
-netdev bridge,id=net0,br=virbr0 \
-net nic,netdev=net0

手动,支持变量展开,需要用双引号,单引号不支持

1
2
3
4
5
6
7
setenv ipaddr 10.0.2.222
setenv serverip 10.0.2.1
setenv bootargs "root=/dev/nfs rw nfsroot=${serverip}:/mnt/nfs/rootfs,nfsvers=3 init=/linuxrc console=ttyAMA0 ip=${ipaddr}"
setenv bootcmd 'tftp 0x60010000 uImage; tftp 0x61000000 vexpress-v2p-ca9.dtb; bootm 0x60010000 - 0x61000000'
saveenv

run bootcmd

linux 内核裁剪之 tftp

说明

用来加载内核

步骤

准备工作

启动 tftp 服务

编译内核

内核需要 uImage 格式,才能被 uboot 加载

1
make LOADADDR=0x60003000 uImage -j$(nproc)

output/arch/arm/boot/uImagedts 放到 tftp 的目录

uboot

手动测试

进入 uboot 后中断

1
2
3
4
5
6
7
sudo qemu-system-arm \
-M vexpress-a9 \
-m 512M \
-kernel ~/downloads/u-boot-2023.04/output/u-boot \
-nographic \
-netdev bridge,id=net0,br=virbr0 \
-net nic,netdev=net0

手动设置变量

1
2
3
4
5
6
# qemu 虚拟机 ip
setenv ipaddr 10.0.2.222

# tftp 服务器 ip
setenv serverip 10.0.2.1
ping 10.0.2.1

下载内核,设备树,启动

1
2
3
tftp 0x60003000 uImage
tftp 0x60500000 vexpress-v2p-ca9.dtb
bootm 0x60003000 - 0x60500000

配置

1
make menuconfig
1
2
3
Environment  --->
[*] Create default environment from file
(uboot.env) Path to default environment file (NEW)

uboot.env

1
2
3
4
ipaddr=10.0.2.222
serverip=10.0.2.1
bootargs=root=/dev/mmcblk0 rw init=/linuxrc ip=10.0.2.222 console=ttyAMA0
bootcmd=tftp 0x60003000 uImage; tftp 0x60500000 vexpress-v2p-ca9.dtb; bootm 0x60003000 - 0x60500000

编译

1
make -j$(nproc)

运行

1
2
3
4
5
6
7
sudo qemu-system-arm \
-M vexpress-a9 \
-m 512M \
-kernel ~/downloads/u-boot-2023.04/output/u-boot \
-nographic \
-netdev bridge,id=net0,br=virbr0 \
-net nic,netdev=net0

linux 内核裁剪之 nfs

说明

用于加载文件系统

步骤

准备工作

启动 nfs 服务,把 rootfs 文件夹放到 nfs 的根目录下

内核

配置

1
make menuconfig
1
2
3
4
5
File systems  --->
[*] Network File Systems --->
<*> NFS client support
<*> NFS client support for NFS version 3
[*] Root file system on NFS

编译

编译结束后,uImage 复制到 tftp 根目录

1
make LOADADDR=0x60003000 uImage -j$(nproc)

uboot

环境变量

uboot.env

1
2
3
4
5
6
ipaddr=10.0.2.222
serverip=10.0.2.1
# 最后的 ip 不能缺
bootargs=root=/dev/nfs rw nfsroot=10.0.2.1:/mnt/nfs/rootfs,nfsvers=3 init=/linuxrc console=ttyAMA0 ip=10.0.2.222
# bootm 中间的 - 不能缺
bootcmd=tftp 0x60003000 uImage; tftp 0x60500000 vexpress-v2p-ca9.dtb; bootm 0x60003000 - 0x60500000

make

1
make -j$(nproc)

测试

1
2
3
4
5
6
7
sudo qemu-system-arm \
-M vexpress-a9 \
-m 512M \
-kernel ~/downloads/u-boot-2023.04/output/u-boot \
-nographic \
-netdev bridge,id=net0,br=virbr0 \
-net nic,netdev=net0

手动,支持变量展开,需要用双引号,单引号不支持

1
2
3
4
5
6
7
setenv ipaddr 10.0.2.222
setenv serverip 10.0.2.1
setenv bootargs "root=/dev/nfs rw nfsroot=${serverip}:/mnt/nfs/rootfs,nfsvers=3 init=/linuxrc console=ttyAMA0 ip=${ipaddr}"
setenv bootcmd 'tftp 0x60003000 uImage; tftp 0x60500000 vexpress-v2p-ca9.dtb; bootm 0x60003000 - 0x60500000'
saveenv

run bootcmd

linux 内核裁剪之 kernel

说明

需要编译

1
2
3
内核
设备树
其它驱动程序

准备

1
https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.191.tar.xz

用法

默认配置

1
make vexpress_defconfig
1
make menuconfig
1
2
3
4
5
# 不想要在 rootfs 里面手动 mknod,需要开这个
Device Drivers --->
Generic Driver Options --->
[*] Maintain a devtmpfs filesystem to mount at /dev
[*] Automount devtmpfs at /dev, after the kernel mounted the rootfs

编译

1
make -j$(nproc)

测试

查看 tty

1
cat .config | grep -i tty
1
CONFIG_CMDLINE="console=ttyAMA0"

运行

1
2
3
4
5
6
7
qemu-system-arm \
-M vexpress-a9 \
-m 512M \
-kernel ~/downloads/linux-5.10.191/output/arch/arm/boot/zImage \
-dtb ~/downloads/linux-5.10.191/output/arch/arm/boot/dts/vexpress-v2p-ca9.dtb \
-nographic \
-append "console=ttyAMA0"