背景

作为一名 python 开发者, 在打包自己写的应用时, 我们一般会使用 pyinstaller / py2exe / cxfreeze 等工具.

但这样打包的结果, 仍存在一些问题困扰着我们:

  • 如果我的应用依赖了 numpy, pyqt / pyside 等库, 打包的体积压不下来.
    -> 我想要一种 “无依赖” 的打包方式, 让体积降到 10mb 以下.
  • 每次更新后, 都要把依赖再打一遍, 整个过程比较长, 体验不好.
    -> 我想要一个快速的打包方案, 且每次只对变化的地方进行 增量更新.
  • 有时候用户电脑上已经安装了 python, 或者用户电脑上已经安装了某些依赖库. 我能不能不打包这些东西, 直接利用现成的?
    -> 我想要一个工具, 来确保用户的电脑环境总是处于 准备就绪 状态.
  • 有时候在客户端会报 “模块缺失”, “路径不正确” 等错误. 由于打包后的目录结构和源代码差异较大, 对我来说也很难排查.
    -> 我想让打包后的目录结构和项目源代码结构保持一致.

如果你有这些需求, 那么 depsland 有希望成为你在寻找的工具.

注意: 当前产品 (v0.3.0) 仍处于早期开发阶段, 不保证满足以上列出的所有需求!

简介

depsland 是针对轻量化的应用分发方案打造的基础服务框架, 用于帮助开发者快速分发 python 应用程序, 并为用户提供简单友好的 程序安装, 升级和管理服务.

depsland 是一个开源项目 (项目地址), 它诞生于 pyportable-installer (项目地址), 现已作为独立的工具供 python 开发者下载和使用.

下载

depsland 提供了两种下载方式, 对于开发者, 可以通过 pip 下载:

pip install depsland

对于普通用户, 特别是没有任何开发经验的用户, 建议通过以下途径下载:

  • 国内下载渠道: 阿里云 oss1
  • 官方项目仓库: github 发布页注意: 由于本人的网络问题, github 仓库只发布无嵌入式解释器的版本, 且发布频率可能落后于国内. 推荐使用国内下载渠道.

注: 截至 2022 年 11 月, 最新发布版本为 0.3.x.


安装

如果你是通过 pip 安装, 本小节可跳过.

你下载得到的是一个 zip 文件 (体积约 65mb), 解压后将得到以下目录:

如何实现程序自动更新 python 服务器 python exe自动更新_python

双击根目录下的 “setup.exe” 开始安装.

请注意选择合适的目录安装, depsland 默认会安装在 C:\ProgramData\Depsland 目录, 但考虑到权限问题, 我们建议你安装在其他盘符下面.

如何实现程序自动更新 python 服务器 python exe自动更新_python打包_02

等待约 30s ~ 1min 安装完成. 新开一个 命令行, 输入 depsland version, 如果显示以下信息, 则说明安装成功:

如何实现程序自动更新 python 服务器 python exe自动更新_python_03

此外, 你还可以输入 depsland -h, 获得所有可用的命令的帮助信息:

如何实现程序自动更新 python 服务器 python exe自动更新_python_04

升级

如果你是通过 pip 安装, 请使用 pip install -U depsland 进行升级.

如果你是通过另一种方式安装, 当前版本 (0.3.x) 暂未提供自升级功能.

注: 我们预计在 0.4.0 时提供自升级功能.

卸载

如果你是通过 pip 安装, 请使用 pip uninstall depsland 进行卸载.

如果你是通过另一种方式安装, depsland 暂未提供关于自身的自动卸载方案.

  • C:\ProgramData\Depsland (或者你的自定义安装目录)
  • 环境变量: DEPSLAND, PATH (~\Depsland\depsland.exe, ~\Depsland\apps\.bin)

使用

depsland 采用类似 poetry 的项目管理方式, 在下面的介绍中, 你会发现它的命令设计与 poetry 有很多相似之处.

0. 使用帮助

在命令行中输入 depsland -h 或者 python3 -m depsland -h 获得所有命令的帮助信息. 你也可以输入 depsland <command> -h 获得某个具体的命令的帮助.

下面会介绍到 depsland 的核心命令, 列表如下:

  1. depsland init: 初始化项目
  2. depsland build: 构建一个项目
  3. depsland publish: 发布你的项目
  4. depsland install & depsland install-dist: (用户) 安装项目

1. 初始化项目

  1. 打开命令行, cd 到你的项目目录, 这里以 “hello-world” 为例
cd ~/my-projects/hello-world
  1. 输入 depsland init, 它将会在该目录下创建一个 “manifest.json” 文件
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N8GIGYlQ-1668558500306)(https://s2.loli.net/2022/11/16/WFDx3Vcdjk1wMRB.png)]
    假设我们的项目目录结构如下:
~/my-projects/hello-world
|= src
   |- main.py
|- README.md
|- requirements.txt
|- manifest.json  # <- 新增一个清单文件

这个清单文件 (“manifest.json”) 作用和 poetry 的 “pyproject.toml” 相似, 它记录了你的项目中的所有资产情况. depsland 使用它来构建和打包.


2. 构建一个项目

  1. 编辑你的清单文件
    下面给出一个示例作为参考:
{
    "appid": "hello_world",  // 应用 id. 采用下划线命名法
    "name": "Hello World",  // 应用名称. 建议使用正常的大小写 (标题命名法)
    "version": "0.1.0",  // 版本号遵循 semver 规范
    "assets": {
        // 键: 填相对于本文件的路径. 请务必将所有要打包的路径都加入进来.
        //     该路径可以是文件, 也可以是目录.
        // 值: 有以下可选值:
        //     all          打包该目录下的全部文件.
        //     all_dirs     保留该目录下的子目录结构, 但不包含文件.
        //     top          打包该目录下的全部文件, 但不包含子目录 (会创建空文件夹).
        //     top_files    打包该目录下的全部文件, 但不包含子目录 (不会创建空文件夹).
        //     top_dirs     保留该目录下的一级子目录结构, 但不包含文件.
        //     root         保留根目录结构, 但不包含子目录或文件 (即只创建根目录作为空文件夹).
        //     此外, 你也可以使用空字符串, 表示 "all".
        "src": "all",
        "README.md": "all",
    },
    "dependencies": {
        // 依赖列表. 按照与 requirements.txt 中相同的定义方式来写.
        // 键: 依赖包名.
        // 值: 版本范围, 留空则表示最新. 如果有多个范围值, 用逗号分隔.
        "argsense": "==0.5.0a0",
        "numpy": "",
        "pyside6": ">=6.1.3",
        "lk-logger": "",
        "lk-utils": ">=2.4.0,<2.5.0",
    },
    "pypi": [
        // 如果你有一些非 pypi 索引的库 (比如你自己写的但没有发布到 pypi 的库), 在这里填写.
        // 格式: 绝对路径或相对 (于本文件) 的路径. 必须是有效的 whl 或 tar.gz 文件.
        "./addons/argsense-0.5.0a0-py3-none-any.whl",
    ],
    "launcher": {  // 在这里定义你的应用该如何被启动.
        "script": "src/main.py --name Alice",  // 格式: <脚本文件> <可选参数>
        "icon": "",  // 启动器图标 (可选), 必须是 ".ico" 格式.
        "cli_tool": true,  // 你的工具是否可以在命令行运行. 开启后, 可使用 `hello_world` 来启动.
        //  注: 如果没开启, 则可以通过 `depsland run hello_world` 来启动.
        "desktop": true,  // 是否生成桌面快捷方式.
        //  如果开启此选项, 建议 icon 也填写. 为你的启动器生成漂亮的图标.
        "start_menu": false,  // 是否添加到开始菜单 (注: 实验性功能!)
        "show_console": true,  // 你的应用启动后, 是否显示一个控制台窗口.
        //  如果你的应用有 gui 界面, 此选项建议关闭.
    }
}

下面这张截图是 depsland 在打包自身时填写的清单信息, 可供参考:

如何实现程序自动更新 python 服务器 python exe自动更新_打包_05

  1. 完成后, 在命令行输入 depsland build 开始构建安装包:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AOvcTTAJ-1668558500307)(https://s2.loli.net/2022/11/16/C1aMmgb6UwAn5YF.png)]
  2. 完成后, 在项目根目录下的 “dist” 中会生成相应的结果:

此时只生成了启动器, 体积约 120kb. 注意现在这个状态是不可用的, 我们需要完成下一步后才能使用.

3. 发布你的应用

  1. 在命令行中输入 depsland publish, 进行一次发布
  2. 此时, 在 hello world 项目的 dist 目录下, 会新增 “setup.exe”, “manifest.pkl” 文件和 “.oss” 文件夹:
  3. 现在, 将这些文件压缩成 zip 文件, 可以将它交给你的用户去使用了.

4. (用户) 安装应用

首先, 请确保用户的电脑上也安装了 depsland 软件 (安装步骤参考 “安装” 章节). 你需要通过主动告知, 手册引导或者用一个检测脚本等方式帮助用户完成此过程.

然后, 用户在收到你发布的安装包后 (这是一个 zip 文件), 解压并双击里面的 “setup.exe” 即可完成安装.

安装过程截图:

如何实现程序自动更新 python 服务器 python exe自动更新_python_06

由于我们在清单文件中配置了 cli_tooldesktop 选项为 true. 所以用户可以通过命令行或桌面快捷方式来启动 hello world:

  • 通过桌面图标启动:
  • 通过命令行启动:
depsland run hello_world

如何实现程序自动更新 python 服务器 python exe自动更新_python_07

关于安装耗时的说明

安装耗时取决于 1) 你的依赖列表是否包含了体积大的依赖库; 2) 是否是首次安装.

通常来说, 包含了较大依赖的情况下, depsland 调用 pip 从 pypi 下载的时间会变长 (几秒到几分钟不等); 而在非首次安装的情况下, depsland 会充分复用已安装的资源, 整个过程则会非常快 (最快甚至能达到毫秒级别).

5. 升级应用

开发者将第二节 (“2. 打包项目”) 中的清单文件进行编辑, 并提升版本号 (例如修改为 “0.2.0”), 重新 depsland build 一下, 得到新的安装包.

新的安装包发给用户后, 用户解压并双击 “setup.exe” 完成升级.

6. 卸载应用

depsland 暂未提供可视化界面的卸载方案.

a) 通过命令行卸载

  1. 用户打开命令行
  2. 输入 depsland uninstall hello_world 完成卸载

b) 手动卸载

用户删除以下路径:

  • C:\ProgramData\Depsland\apps\hello_world\0.1.0
  • C:\ProgramData\Depsland\apps\.bin\hello_world.exe
  • C:\ProgramData\Depsland\apps\.venv\hello_world

原理说明

depsland 的项目结构

注: 下图中, |= 表示文件夹, |- 表示文件, # 后面是注释.

depsland  # 这里是根目录, 在安装完成后, 会加入到用户的 PATH 环境变量中.
|= apps
   |= .bin  # 在这里放置可执行文件, 例如 "hello_world.exe". 
   |        # 该路径也会加入到用户的 PATH 环境变量中, 因此用户可以在控制台输入 "hello_world" 直接调用.
      |- hello_world.exe
   |= .venv  # 在这里创建每个 app 的 python 虚拟环境.
      |= hello_world
         |= lk_logger
         |= numpy
         |= ...
   # 除 ".bin" 和 ".venv" 这两个以点号开头的特殊目录外, 其他目录都是第三方应用目录. 
   # 这些第三方应用的目录是以 <appid> 作为名称.
   |= hello_world  # 在该目录下就是应用有关的文件, 里面的内容各不相同, 与应用自身有关.
      |= src
         |- main.py
      |- README.md
      |- CHANGELOG.md
   |= ...  # 更多应用
|= build  # 该目录下存放用于构建 depsland 自身的脚本和资产文件.
|= chore  # 一些杂项.
|= conf  # 配置文件. 目前仅有一个配置 "depsland.yaml".
   |- depsland.yaml  # 该文件会被 depsland.config 模块读取.
|= depsland  # depsland 的开源代码.
|= docs  # 技术文档和用户手册.
|= oss  # 本地的第三方资源存储目录, 用来模拟 oss 服务的存储.
|       # 当我们使用 depsland publish 的时候, 应用的资产文件都会被复制一份到这里.
|= pypi  # 缓存从 pypi 网站下载的 python 第三方库, 用于跨应用的依赖复用, 以及快速生成 apps/.venv 下的虚拟环境.
   |= cache  # pip 命令的自定义缓存目录
   |= downloads  # pip 下载的 whl, tar.gz, zip 文件
   |= installed  # pip 安装的结果
   |             # 在这里以 <package_name>/<version>/<pip_install_results> 的形式存在.
   |= index  # 索引目录, 用于查询第三方库之间的依赖关系和路径地图.
|= python  # 一个便携式 python 解释器 (3.10 版本)
|= temp  # 临时文件目录, 用于放置运行时产生的临时文件. 在 depsland 运行结束后会自动清理.
|- .depsland_project  # 用于帮助 depsland 检查自己是处于项目开发模式还是发行包模式, 具体见该文件内的说明.
|- manifest.json  # depsland 自身的清单文件
|- CHANGELOG.zh.md  # 更新日志
|- README.zh.md  # 自述文档

depsland 命令详解

1. depsland init

depsland init 会在目标项目的根目录下生成一个 “manifest.json”, 叫做清单文件.

清单文件包含以下键:

  • appid: 一个独立的应用 id. 用于创建其在用户端的 depsland/apps/<appid> 应用目录.
  • name: 用于启动器名称, 例如 “Hello World.exe”. 当创建桌面快捷方式时, 用户将在桌面看到 “Hello World” 启动器图标.
  • version: 版本号.
  • assets: 资产文件, 指要打包的所有目录/文件清单. 请务必填写所有需要的资源, 否则在用户端会报文件/模块缺失错误.
  • dependencies: python 依赖列表. 参考 requirements.txt 的方式来填写. 请确保这些依赖都能在官方 pypi (或者镜像站) 中能够下载到.
  • pypi: 自定义的依赖资源. 指未上传到 pypi 网站的自定义包, 格式为 “.whl” 或 “.tar.gz”.
  • launcher: 启动器设置. 用于指示 depsland 如何创建启动器. 目前支持的形式有: 命令行, 桌面快捷方式, 开始菜单.
2. depsland build

depsland build 命令会在目标项目的根目录下的 dist 目录 (如果没有会自动创建) 生成待发布目录.

示例如下:

hello_world_project
|= dist
   |= hello_world-0.1.0  # 生成该目录
      |- launcher.exe  # 目录下有一个启动器文件

请注意此时的启动器是不可用的. 因为我们还没有打包清单文件中的资产.

3. depsland publish

depsland publish 会根据清单文件内容, 再与旧版本的清单文件 (如果曾经发布过的话) 进行比对, 找到 “新增的”/“变化的”/“移除的” 资产内容, 进行 增量更新.

更新过的资产会被保存到 depsland/oss/apps/<appid> 目录. 同时会被软链接到 hello_world_project/dist/hello_world-0.1.0/.oss 目录.

此时 dist 内容如下:

hello_world_project
|= dist
   |= hello_world-0.1.0
      |= .oss  # 软链接, 来自 `depsland/oss/apps/<appid>`
      |- manifest.pkl  # 结构化的清单文件 (二进制格式)
      |- setup.exe  # 安装器
      |- launcher.exe

depsland 是如何实现增量更新的

depsland 根据清单文件 (主要是 “assets” 键) 的内容生成一个叫做 AssetInfo 的对象, 每个 AssetInfo 对象存储了以下信息:

  • 资产备份策略 (scheme: all, all_files, all_dirs, top, top_files, top_dirs, root)
  • 文件的哈希值 (hash)
  • 文件的最近修改时间 (utime)
  • 文件的路径哈希值 (uid)

当进行比对时, 会综合比较 scheme, hash, utime 的差异, 以此确定新旧版本的清单文件是否存在差异.

如何实现程序自动更新 python 服务器 python exe自动更新_python_08

请注意 uid 在相同项的比对中是一定相同的, 所以对于有差异的项, 将 uid 作为远端存储的文件名, 即可实现覆盖上传.


  1. 文件有效期至 2022 年 11 月 23 日. 请及时下载. ↩︎