在上一篇文章中,介绍了如何集成加强版的geodata文件到openwrt。
但是这个加强版的geodata每日更新。按之前的办法,要去github查看新版本的信息,手动更新。太麻烦了……

这里面提供一个python脚本,可以方便的自动更新Makefile文件到最新版本。

使用方法将脚本的内容保存成文件。比如up_geodata.sh文件。
然后运行脚本,指定要更新的Makefiel文件路径:

python3 up_geodata.sh -b Makefile

参数-b表示备份旧的文件,不想备份可以不加。
mac或者linux,还可以直接添加可执行权限,当成应用来运行:

chmod +x up_geodata.sh

以下是脚本内容:

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import requests
import re
import argparse
import shutil
import os
from datetime import datetime

# GitHub API URL
GITHUB_API_URL = "https://api.github.com/repos/Loyalsoldier/v2ray-rules-dat/releases/latest"

# 需要下载的 SHA256 文件
FILES = {"geoip": "geoip.dat.sha256sum", "geosite": "geosite.dat.sha256sum"}


def get_latest_version():
    """获取 GitHub 最新 Release 版本号"""
    response = requests.get(GITHUB_API_URL)
    if response.status_code == 200:
        release_data = response.json()
        return release_data['tag_name']  # 获取最新版本号
    else:
        raise Exception("Failed to fetch release data from GitHub API")


def download_sha256sum_file(version, file_key):
    """下载 SHA256 校验文件"""
    file_name = FILES[file_key]
    url = f"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/download/{version}/{file_name}"
    response = requests.get(url)

    if response.status_code == 200:
        return response.text.strip()  # 直接返回文本内容
    else:
        raise Exception(
            f"Failed to download {file_name} for version {version}")


def extract_hash(file_content):
    """解析 SHA256 文件中的哈希值"""
    return file_content.split()[0]  # 取第一列的哈希值


def backup_makefile(makefile_path):
    """备份 Makefile"""
    if os.path.exists(makefile_path):
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_path = f"{makefile_path}.bak_{timestamp}"
        shutil.copy(makefile_path, backup_path)
        print(f"✅ 备份已创建: {backup_path}")
    else:
        raise FileNotFoundError(f"❌ 未找到 Makefile: {makefile_path}")


def update_makefile(makefile_path, version, geoip_hash, geosite_hash):
    """修改 Makefile,更新版本号和哈希值"""
    with open(makefile_path, "r") as file:
        makefile_content = file.read()

    # **修正版本号**
    makefile_content = re.sub(r"(GEODATA_VER:=)\d+", r"\g<1>" + version,
                              makefile_content)

    # **修正 geoip.dat 的 HASH**
    makefile_content = re.sub(
        r"(define Download/geoip\s+.*?\n\s+HASH:=)[a-f0-9]+",
        r"\g<1>" + geoip_hash,
        makefile_content,
        flags=re.DOTALL)

    # **修正 geosite.dat 的 HASH**
    makefile_content = re.sub(
        r"(define Download/geosite\s+.*?\n\s+HASH:=)[a-f0-9]+",
        r"\g<1>" + geosite_hash,
        makefile_content,
        flags=re.DOTALL)

    with open(makefile_path, "w") as file:
        file.write(makefile_content)

    print(
        f"✅ Makefile 已成功更新: 版本 {version}, GeoIP Hash: {geoip_hash}, Geosite Hash: {geosite_hash}"
    )


def main():
    parser = argparse.ArgumentParser(
        description="更新 Makefile 中的 v2ray-geodata 版本号和 HASH")
    parser.add_argument("makefile", help="需要更新的 Makefile 路径")
    parser.add_argument("-b",
                        "--backup",
                        action="store_true",
                        help="是否备份 Makefile")
    args = parser.parse_args()

    makefile_path = args.makefile

    try:
        # **只有在使用 -b 时才进行备份**
        if args.backup:
            backup_makefile(makefile_path)

        # **获取最新版本号**
        version = get_latest_version()
        print(f"📌 最新版本号: {version}")

        # **获取 SHA256 哈希值**
        geoip_hash = extract_hash(download_sha256sum_file(version, "geoip"))
        geosite_hash = extract_hash(download_sha256sum_file(
            version, "geosite"))

        print(f"📌 GeoIP Hash: {geoip_hash}")
        print(f"📌 Geosite Hash: {geosite_hash}")

        # **更新 Makefile**
        update_makefile(makefile_path, version, geoip_hash, geosite_hash)

    except Exception as e:
        print(f"❌ 发生错误: {e}")


if __name__ == "__main__":
    main()

早年写过一篇旧的同名文章。介绍过如何在openwrt编译时,直接集成一个自己想要的geodata文件。
只是当年才疏学浅,用了最土的方式,直接修改openwrt源文件的方式来实现。
如今研究过openwrt的项目结构后,发现通过添加外部feeds来覆盖源目标的方式才是最正确的方法。

这个方法也适用于要添加其它的自定义项目。
首先,将要添加的项目的文件,这里geodata只有一个Makefile文件。保存到适当的位置。
以我保存的路径为例:

/home/ubuntu/openwrt/feeds/v2ray-geodata/Makefile

然后,在openwrt项目,根目录下的feeds.conf.default第一行添加以下一行:

src-link custom /home/ubuntu/openwrt/feeds

为什么要在第一行?因为feeds的优先级是根据自上而下的加载顺序决定的。最先加载的优先级最高,可以覆盖后面原生自带的相同项目。
这里也可以看出来,在/home/ubuntu/openwrt/feeds的路径下,还可以放入其它多个自己想要添加的项目。
最后就是正常的编译流程:

cd lede
git pull
./scripts/feeds update -a
./scripts/feeds install -a
make menuconfig
make download -j8
make V=s -j$(nproc)

下面是我的加强版geodata项目Makefile文件内容,可以直接保存使用。

# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2021-2022 ImmortalWrt.org

include $(TOPDIR)/rules.mk

PKG_NAME:=v2ray-geodata
PKG_RELEASE:=1

PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=winger zhang <winger.zhang@gmail.com>

include $(INCLUDE_DIR)/package.mk

GEODATA_VER:=202503192212
GEOIP_FILE:=geoip.dat.$(GEODATA_VER)
define Download/geoip
  URL:=https://github.com/Loyalsoldier/v2ray-rules-dat/releases/download/$(GEODATA_VER)/
  URL_FILE:=geoip.dat
  FILE:=$(GEOIP_FILE)
  HASH:=e32b80017d1dea91bc36c4e7ed07a5e7f200356415e678a8a6f825fb521f7b68
endef

GEOSITE_FILE:=geosite.dat.$(GEODATA_VER)
define Download/geosite
  URL:=https://github.com/Loyalsoldier/v2ray-rules-dat/releases/download/$(GEODATA_VER)/
  URL_FILE:=geosite.dat
  FILE:=$(GEOSITE_FILE)
  HASH:=57c476f6e50737c5fdf68778ec1615fb2017f75886a6bb0252927c314e5904b7
endef

define Package/v2ray-geodata/template
  SECTION:=net
  CATEGORY:=Network
  SUBMENU:=IP Addresses and Names
  URL:=https://www.v2fly.org
  PKGARCH:=all
endef

define Package/v2ray-geoip
  $(call Package/v2ray-geodata/template)
  TITLE:=GeoIP List for V2Ray
  VERSION:=$(GEODATA_VER)-$(PKG_RELEASE)
  LICENSE:=CC-BY-SA-4.0
endef

define Package/v2ray-geosite
  $(call Package/v2ray-geodata/template)
  TITLE:=Geosite List for V2Ray
  VERSION:=$(GEODATA_VER)-$(PKG_RELEASE)
  LICENSE:=MIT
endef

define Build/Prepare
    $(call Build/Prepare/Default)
ifneq ($(CONFIG_PACKAGE_v2ray-geoip),)
    $(call Download,geoip)
endif
ifneq ($(CONFIG_PACKAGE_v2ray-geosite),)
    $(call Download,geosite)
endif
endef

define Build/Compile
endef

define Package/v2ray-geoip/install
    $(INSTALL_DIR) $(1)/usr/share/v2ray
    $(INSTALL_DATA) $(DL_DIR)/$(GEOIP_FILE) $(1)/usr/share/v2ray/geoip.dat
endef

define Package/v2ray-geosite/install
    $(INSTALL_DIR) $(1)/usr/share/v2ray
    $(INSTALL_DATA) $(DL_DIR)/$(GEOSITE_FILE) $(1)/usr/share/v2ray/geosite.dat
endef

$(eval $(call BuildPackage,v2ray-geoip))
$(eval $(call BuildPackage,v2ray-geosite))

最后,如何快速的更新Makefile文件,以编译打包最新的geodata文件。可以看我的另外一篇文章

用来生成可用在代码中的,指定长度hex字符脚本。默认为16个字条表示。

import argparse
import secrets

def generate_random_hex_string(length):
    # 生成指定长度的随机字节
    random_bytes = secrets.token_bytes(length)
    
    # 将每个字节转化为以0x开头的十六进制字符串
    hex_string = ','.join(f'0x{byte:02x}' for byte in random_bytes)
    
    return hex_string

def main():
    # 命令行参数解析
    parser = argparse.ArgumentParser(description="生成随机十六进制字节串")
    parser.add_argument('length', type=int, nargs='?', default=16, 
                        help="生成十六进制随机数的长度,默认为16")
    
    args = parser.parse_args()
    
    # 生成随机十六进制字符串
    result = generate_random_hex_string(args.length)
    
    # 输出结果
    print(result)

# 检查是否直接运行该脚本
if __name__ == '__main__':
    main()

以下的C++方法可以判断一个文本是否符合base64的编码规则。

bool IsBase64String(std::string_view str)
{
    auto length = str.length();

    if (!length || length % 4)
    {
        return false;
    }

    if (str[length - 1] == '=')
    {
        --length;
        if (str[length - 1] == '=')
        {
            --length;
        }
    }

    auto base64Part = str.substr(0, length);
    for (auto& c : base64Part)
    {
        if (!(std::isalnum(c) || c == '+' || c == '/'))
        {
            return false;
        }
    }

    return true;
}

需要从文本读入一组二进制数据。希望尽可能多的兼容各种文本形式。比如0x1234567890abcdef或1234567890abcdef或12 34 45 78 90 ab cd ef或12,34,56,78,90,ab,cd,ef或者0x12,0x34,0x56,0x78,0x90,0xab,0xcd,0xef这样的文本都可以正确的解析成二进制数据,还要需要有无效格式的判断。
以下是C++实现:

std::vector<uint8_t> ParseHexToBytes(std::string_view input)
{
    std::vector<uint8_t> bytes;
    bytes.reserve(1024);

    std::string currentByte;
    currentByte.reserve(2);

    for (size_t i = 0; i < input.size(); ++i)
    {
        char c = std::tolower(input[i]);

        if (c == '0' && i + 1 < input.size() && (input[i + 1] == 'x' || input[i + 1] == 'X'))
        {
            ++i;
            continue;
        }

        if (std::ispunct(c) || std::isblank(c))
        {
            continue;
        }

        if (!std::isxdigit(c))
        {
            return {};
        }

        currentByte += c;

        if (currentByte.size() == 2)
        {
            uint8_t byte = static_cast<uint8_t>(std::stoi(currentByte, nullptr, 16));
            bytes.push_back(byte);
            currentByte.clear();
        }
    }

    return bytes;
}