[{"content":"用坐标完成 DMG 布局 在第 1 篇中，我们介绍了 create-dmg 工具，并准备了仅含 .app 的暂存文件夹和背景图片。\n现在来到最后一块拼图——布局。DMG 窗口要多大、应用图标和 Applications 快捷方式各放在哪里，这一切都由传给 create-dmg 的坐标来决定。本文将逐一设计这些坐标。\n与第 1 篇相同，FocusTimer、坐标值等均为示例。\ncreate-dmg 完整命令 首先来看完整命令的全貌。这是为示例应用 FocusTimer 1.0 制作 DMG 的命令：\ncreate-dmg \\ --volname \u0026#34;FocusTimer 1.0\u0026#34; \\ --background build/dmg-background.png \\ --window-pos 200 120 \\ --window-size 600 400 \\ --icon-size 100 \\ --icon \u0026#34;FocusTimer.app\u0026#34; 175 190 \\ --hide-extension \u0026#34;FocusTimer.app\u0026#34; \\ --app-drop-link 425 190 \\ --no-internet-enable \\ \u0026#34;build/FocusTimer-1.0.dmg\u0026#34; \\ \u0026#34;build/dmg-stage/\u0026#34; 最后两行是输出和输入：\nbuild/FocusTimer-1.0.dmg — 将要生成的 DMG 文件路径 build/dmg-stage/ — 第 1 篇中仅存放了 .app 的暂存文件夹 其上以 -- 开头的行是设计窗口和图标的选项。我们逐一来看。\n理解坐标系 在看各选项之前，先搞清楚坐标是如何工作的。\ncreate-dmg 的位置坐标均为 DMG 窗口内部的坐标，单位是第 1 篇中介绍的点 (point)。\n原点 (0, 0) 是窗口的左上角。 X 向右增大，Y 向下增大。（注意：与数学坐标系不同，Y 向下增大。） 若窗口大小设为 600 400，坐标在横向 0~600、纵向 0~400 范围内移动。 图标的位置坐标指向该图标的中心点，而非图标的左上角。\n各选项逐一解析 选项 值（示例） 含义 --volname \u0026quot;FocusTimer 1.0\u0026quot; DMG 挂载时显示的卷名 --background build/dmg-background.png 窗口背景图片（第 1 篇中准备的） --window-pos 200 120 窗口在屏幕上打开的位置（X, Y） --window-size 600 400 窗口大小（宽, 高 — 单位：点） --icon-size 100 窗口内图标的大小（单位：点） --icon \u0026quot;FocusTimer.app\u0026quot; 175 190 应用图标的位置（窗口内 X, Y） --hide-extension \u0026quot;FocusTimer.app\u0026quot; — 隐藏图标名称中的 .app 扩展名 --app-drop-link 425 190 Applications 文件夹快捷方式的位置（窗口内 X, Y） --no-internet-enable — 关闭已弃用的\u0026quot;互联网启用\u0026quot;功能 以下几点值得特别说明：\n--window-pos 与 --window-size — 这两个容易混淆。--window-pos 是用户打开 DMG 时窗口出现在屏幕（桌面）的哪个位置，--window-size 是窗口本身的大小。坐标设计中重要的是 --window-size。\n--icon-size 100 — 窗口内的图标以 100 点的大小绘制。设计背景图片时也需要知道这个值，因为图标占据的 100 点（= 背景图片中的 200 像素）空间需要在背景中留空。\n--icon 与 --app-drop-link — 这两个是窗口左右两侧的主角。--icon 是用户需要拖动的应用图标，--app-drop-link 是拖放目标——Applications 文件夹快捷方式。使用 --app-drop-link 无需单独创建符号链接，create-dmg 会自动创建并放置 Applications 快捷方式。\n--hide-extension — 不加此选项，图标下方会显示包含扩展名的 FocusTimer.app。隐藏后只会整洁地显示 FocusTimer。\n--no-internet-enable — \u0026ldquo;互联网启用 (internet-enable)\u0026ldquo;是旧版 Safari 自动处理下载 DMG 的旧功能，现已废弃。添加此选项后，create-dmg 会跳过该处理步骤及相关警告。\n坐标设计 — 放置两个图标 现在来到核心部分。窗口大小设为 600 400，在其中决定应用图标和 Applications 快捷方式各放在哪里。\n示例命令中的设置如下：\n应用图标：--icon \u0026quot;FocusTimer.app\u0026quot; 175 190 Applications 快捷方式：--app-drop-link 425 190 这些数字的含义解析如下：\nX 轴（横向）— 175 与 425\n窗口宽度为 600 点，其中心为 300。将两个图标分别置于 175 和 425，则两者分别距中心向左 125、向右 125。即两个图标以中心为基准左右对称排列。应用图标在左、Applications 在右，形成\u0026quot;从左向右拖拽\u0026quot;的自然方向感。\nY 轴（纵向）— 两者均为 190\n窗口高度为 400 点，其中心为 200。将两个图标置于相同的 190，则并排在同一高度，略高于正中心（200）。由于图标名称标签在图标下方，稍微上移后，包含名称标签在内视觉上恰好居中。\n坐标没有唯一正确的答案。上述值只是\u0026quot;在 600×400 窗口中左右对称并排放置两个 100 点图标\u0026quot;的一种均衡示例。请根据你自己的背景设计自由调整。\n背景图片与坐标对齐 这里再次用到第 1 篇的 @2x 惯例。\n图标坐标（175,190、425,190）是窗口的点坐标。而背景图片是其 2 倍大小的 1200×800 像素。在背景图片中绘制箭头时，需要以窗口坐标乘以 2 的像素坐标来思考：\n背景图片像素坐标 = 窗口点坐标 × 2 将示例中两个图标转换为背景图片像素坐标如下：\n元素 窗口坐标（点） 背景图片坐标（像素） 应用图标中心 (175, 190) (350, 380) Applications 中心 (425, 190) (850, 380) 因此，在背景图片（1200×800）上绘制视线引导箭头时，必须绘制在两个图标位置——像素 (350, 380) 与 (850, 380) 之间。图标大小为 100 点 = 200 像素，因此留出每个图标占据的 200 像素区域，将箭头放在中间的空间里即可。\n使背景图片的图示与 create-dmg 的图标坐标以相同基准对齐，正是 DMG 设计的核心。若两者错位，箭头就会偏离图标或叠在图标上。\n验证与微调 执行命令后，build/FocusTimer-1.0.dmg 会生成。请双击打开生成的 DMG 亲自查看。\n背景是否完整填满了窗口？（若缩在角落 → 第 1 篇的 DPI 陷阱，用 sips 规范化为 144） 应用图标和 Applications 是否恰好落在背景箭头的两端？ 图标名称标签是否被截断或重叠？ 坐标第一次很难一步到位。稍微调整 --icon 和 --app-drop-link 的数值，重新执行命令，肉眼确认，反复几次即可。一旦确定好坐标，之后几乎不需要再改动。\n每次发布复用 — 自动化 DMG 设计实质上是一次性工作。背景图片和坐标一旦确定，之后所有发布都使用完全相同的设置。\n因此，建议将 create-dmg 调用作为发布构建脚本的一个步骤。脚本中通常是这样的流程：\n将签名、公证、staple 完成的 .app 复制到暂存文件夹（第 1 篇） 复制背景图片后用 sips 将 DPI 规范化为 144（防止第 1 篇的陷阱） 使用上面设计的坐标执行 create-dmg 清理暂存文件夹等临时文件 每次发布只有版本号（--volname 中的 1.0，输出文件名中的 FocusTimer-1.0.dmg）会变化，因此只需将那部分提取为脚本变量，就无需每次修改命令。背景图片路径和所有坐标设为固定值。\n建议让脚本每次自动执行背景图片的 DPI 规范化（sips）。这样即使重新导出设计原稿替换后忘记规范化 DPI，脚本也会始终自动修正。\n系列小结 历经两篇的 DMG 设计全部完成。现在你已经准备好以下内容：\n✅ (第 1 篇) create-dmg + .app 暂存文件夹 + @2x 且 DPI 规范化的背景图片 ✅ (第 2 篇) 窗口与图标坐标设计 + 背景像素坐标与窗口点坐标对齐 + 自动化 核心要点再次总结：\nDMG 窗口是 600×400 点，背景图片是其 2 倍的 1200×800 像素（@2x）。 背景 PNG 的 DPI 必须规范化为 144 — 这样背景才能精确填满窗口。 图标坐标是窗口内部的点坐标，背景图示坐标是其 2 倍的像素坐标 — 以相同基准对齐两者。 将确定好的设计放入构建脚本，每次发布时原样复用。 一支小小的箭头、几个点的图标位置偏差，都会影响用户的第一印象。.dmg 窗口是用户与你的应用初次相遇的地方，值得用心设计一次。\n参考资料 create-dmg — GitHub sips — macOS 命令手册 ","permalink":"https://hobbyworker.me/zh-hans/dev/2026-05-21-design-macos-dmg-2-layout-coordinates/","summary":"\u003ch1 id=\"用坐标完成-dmg-布局\"\u003e用坐标完成 DMG 布局\u003c/h1\u003e\n\u003cp\u003e在\u003ca href=\"../2026-05-20-design-macos-dmg-1-create-dmg-and-background/\"\u003e第 1 篇\u003c/a\u003e中，我们介绍了 \u003ccode\u003ecreate-dmg\u003c/code\u003e 工具，并准备了仅含 \u003ccode\u003e.app\u003c/code\u003e 的暂存文件夹和背景图片。\u003c/p\u003e\n\u003cp\u003e现在来到最后一块拼图——\u003cstrong\u003e布局\u003c/strong\u003e。DMG 窗口要多大、应用图标和 \u003ccode\u003eApplications\u003c/code\u003e 快捷方式各放在哪里，这一切都由传给 \u003ccode\u003ecreate-dmg\u003c/code\u003e 的\u003cstrong\u003e坐标\u003c/strong\u003e来决定。本文将逐一设计这些坐标。\u003c/p\u003e","title":"macOS 应用分发用 DMG 设计 (2): 窗口与图标坐标设计及自动化"},{"content":"用户最先看到的界面 直接分发 macOS 应用时，用户会下载 .dmg 文件并双击打开。那一刻，一个 Finder 窗口弹出。这个窗口就是用户与你的应用初次相遇的第一个界面。\n一个制作精良的 .dmg 窗口通常长这样——背景铺着引导图示，左侧放着应用图标，右侧放着 Applications 文件夹快捷方式。用户将应用图标拖到 Applications 一侧即完成安装。这种\u0026quot;拖拽安装\u0026quot;是 macOS 独立开发者应用的事实标准安装体验。\n这个窗口不会自动生成。背景图片、窗口大小、图标的位置和尺寸，都需要你亲自指定。本系列介绍如何设计这个 .dmg 窗口。\n本系列以已完成签名、公证和 staple 的 .app 在手为前提，即处于分发前一步的应用。该过程在 Developer ID 直接分发系列中介绍过。.dmg 是直接分发（Developer ID）渠道的产出物，与 Mac App Store 渠道无关。\n本系列将完成的内容 共两篇，最终你将具备以下内容：\n(第 1 篇，本文) create-dmg 工具 + 仅含 .app 的暂存文件夹 + 背景图片准备 (第 2 篇) 窗口、图标和拖放目标的坐标设计 + 背景图片与坐标对齐 + 自动化 系列结束时，你应该已经准备好以下内容：\n仅隔离存放 .app 的暂存文件夹 符合 @2x 视网膜屏惯例且 DPI 已规范化的背景图片 窗口大小和图标位置均以坐标确定的 create-dmg 命令 可在每次发布时原样复用的形态 示例应用 — FocusTimer 与前几个系列相同，以虚构的 macOS 应用 FocusTimer（管理专注时间的简单计时器应用）为例。FocusTimer、路径、坐标值等均为示例，实际使用时请替换为你自己的应用。\n什么是 DMG DMG（.dmg）是 macOS 的磁盘镜像 (disk image) 文件。可以将其理解为一个\u0026quot;虚拟磁盘\u0026quot;——单个文件内部完整包含了文件夹和文件结构。用户双击 .dmg 后，macOS 会像挂载磁盘一样将其挂载，其内容以 Finder 窗口的形式打开。\n在应用分发中使用 DMG 的原因：\n应用（.app）实际上是一个文件夹。将文件夹直接发布到互联网比较困难，但用 DMG 包裹后就变成了单个文件。 可以在 DMG 窗口内放入 Applications 文件夹快捷方式，引导用户一次拖拽即可完成安装。 可以设计窗口的外观——背景、图标位置等，让安装体验更加简洁。 工具 — create-dmg 手动制作 DMG 需要经过多个步骤：用 hdiutil 创建镜像、挂载、用 AppleScript 操作 Finder 窗口放置图标，再卸载等。繁琐且容易出错。\ncreate-dmg 是将这一流程浓缩为单条命令的开源工具。将背景图片、窗口大小、图标坐标作为选项传入，它就会生成完成的 .dmg。\n在 Developer ID 直接分发系列第 1 篇中安装前置工具时已经安装过了。若尚未安装，请通过 Homebrew 安装：\nbrew install create-dmg 名为 create-dmg 的项目有两个。本文使用的是通过 Homebrew 安装的命令行工具（create-dmg/create-dmg）。请勿与选项名称不同的另一个项目混淆。\n第 1 步 — 仅含 .app 的暂存文件夹 DMG 中应该整洁地只放一个应用图标。然而构建和导出应用后，结果文件夹中除了 .app 之外，还有 DistributionSummary.plist、打包日志等副产物文件。直接将这个文件夹制作成 DMG，副产物也会暴露在窗口中。\n因此，需要将 .app 单独复制到空文件夹中隔离，以该文件夹作为制作 DMG 的原料。这个隔离用文件夹通常称为暂存 (staging) 文件夹。\n# 将 .app 单独复制到空文件夹中，避免与副产物混在一起 DMG_STAGE=\u0026#34;build/dmg-stage\u0026#34; rm -rf \u0026#34;$DMG_STAGE\u0026#34; mkdir -p \u0026#34;$DMG_STAGE\u0026#34; cp -R \u0026#34;build/export/FocusTimer.app\u0026#34; \u0026#34;$DMG_STAGE/\u0026#34; 现在 build/dmg-stage/ 中只有 FocusTimer.app 一个文件。制作 DMG 时将 create-dmg 指向此文件夹，窗口中就会整洁地只显示应用图标。（Applications 文件夹快捷方式将在第 2 篇中通过 create-dmg 选项自动生成。）\n第 2 步 — 准备背景图片 DMG 窗口设计的核心是背景图片。这里是最容易卡住的地方，我们一步步来看。\n画布尺寸 — 为何是 1200×800 第 2 篇中会向 create-dmg 传递 --window-size 600 400 来设定 DMG 窗口大小。此时 600 和 400 的单位不是像素，而是点 (point)。点是 macOS 使用的逻辑坐标单位。\n在视网膜屏 (Retina) 显示器上，1 点对应 2 个像素。因此，要完整清晰地填满 600×400 点的窗口，背景图片必须是其 2 倍大小，即 1200×800 像素。这就是 @2x（2 倍分辨率）惯例。\n总结：窗口是 600×400 点，背景图片是 1200×800 像素。 这个 2 倍关系在第 2 篇设定坐标时还会再次用到，非常重要。\n背景中绘制什么 这里纠正一个常见误解：背景图片中不要绘制应用图标或 Applications 文件夹图标。 这些图标由 create-dmg 作为实际文件放置在窗口上（第 2 篇）。\n背景图片中只放装饰和引导元素：\n从应用图标指向 Applications 文件夹的箭头 — 最常见的设计，引导用户的视线和手\u0026quot;向这个方向拖拽\u0026quot;。 如有需要，可加上\u0026quot;拖到此处\u0026quot;等简短引导文字 品牌色或淡淡的背景纹理 也就是说，背景图片是**\u0026ldquo;留空图标放置位，只绘制连接两者的引导线的图\u0026rdquo;**。左右各留出图标将要占用的位置（第 2 篇中各为 100 点 = 200 像素），在中间放置箭头。\n关键陷阱 — DPI 背景图片中最常踩到的陷阱是 DPI（每英寸像素数）。\n用图像编辑器导出 PNG 时，文件中除了像素数（1200×800）之外，还会存储 DPI 值。Finder 在绘制 DMG 背景时会读取这个 DPI，反算出\u0026quot;该图片是多少点的尺寸\u0026quot;。计算大致如下：\n点尺寸 = 像素数 ÷ (DPI ÷ 72) 若 DPI 为 144：1200 ÷ (144 ÷ 72) = 1200 ÷ 2 = 600 点。恰好填满窗口（600 点）。✅ 然而，某些图像编辑器（如 Pixelmator Pro）导出时会嵌入任意 DPI 值（如 338）。若 DPI 为 338：1200 ÷ (338 ÷ 72) ≈ 256 点。比窗口（600 点）小得多，背景就会缩小成一团，塞在窗口左上角。❌ 像素数同样是 1200×800，却因为一个 DPI 值导致背景显示异常。第一次遇到时真的很难找到原因。\n解决方案是将导出 PNG 的 DPI 强制统一为 144。macOS 内置的 sips 命令一行即可搞定：\nsips -s dpiWidth 144 -s dpiHeight 144 dmg-background.png 执行这一行后，无论编辑器嵌入了何种 DPI，都会被设为 144，1200×800 像素将被准确解读为 600×400 点。\n要点 — 背景图片 ① 制作为 1200×800 像素，② 导出后用 sips 将 DPI 规范化为 144。遵守这两点就能避开 DPI 陷阱。\n第 1 篇小结 跟到这里，你现在已经准备好以下内容：\n✅ 安装了 create-dmg 工具，并了解其作用 ✅ 仅隔离存放 .app 的暂存文件夹（build/dmg-stage/） ✅ 以 1200×800 像素制作且 DPI 已规范化为 144 的背景图片 素材已经准备就绪。但还没有一个窗口来显示背景图片并放置图标。窗口要多大、应用图标和 Applications 快捷方式各放在哪里——这一切都由传给 create-dmg 的坐标来决定。\n下一篇将逐一设计这些坐标，介绍如何让背景像素坐标与窗口点坐标对齐以使背景中的箭头恰好落在两个图标之间，以及如何自动化整个流程以在每次发布时复用。\n","permalink":"https://hobbyworker.me/zh-hans/dev/2026-05-20-design-macos-dmg-1-create-dmg-and-background/","summary":"\u003ch1 id=\"用户最先看到的界面\"\u003e用户最先看到的界面\u003c/h1\u003e\n\u003cp\u003e直接分发 macOS 应用时，用户会下载 \u003ccode\u003e.dmg\u003c/code\u003e 文件并双击打开。那一刻，\u003cstrong\u003e一个 Finder 窗口弹出\u003c/strong\u003e。这个窗口就是用户与你的应用初次相遇的第一个界面。\u003c/p\u003e\n\u003cp\u003e一个制作精良的 \u003ccode\u003e.dmg\u003c/code\u003e 窗口通常长这样——背景铺着引导图示，左侧放着应用图标，右侧放着 \u003ccode\u003eApplications\u003c/code\u003e 文件夹快捷方式。用户将应用图标\u003cstrong\u003e拖到\u003c/strong\u003e \u003ccode\u003eApplications\u003c/code\u003e 一侧即完成安装。这种\u0026quot;拖拽安装\u0026quot;是 macOS 独立开发者应用的事实标准安装体验。\u003c/p\u003e","title":"macOS 应用分发用 DMG 设计 (1): create-dmg 与背景图片准备"},{"content":"将构建发送到 App Store 的路径 第 1 篇创建了 MAS 构建目标，第 2 篇创建了区分两个渠道的配置文件和代码分支。FocusTimer MAS 目标现在已成为可以上架 App Store 的形态。\n本篇（最终篇）将搭建将该构建上传到 App Store Connect 的路径，并介绍如何验证两个渠道在未来不会被破坏，从而收尾本系列。\n与第 1、2 篇相同，FocusTimer、com.example.FocusTimer.mas、Team ID ABCDE12345 等均为示例值。\n第 1 步 — 用于上传的 ExportOptions-MAS.plist 在直接分发系列中，我们提到将归档导出为分发用时，xcodebuild -exportArchive 命令会读取 ExportOptions.plist。MAS 渠道需要独立的导出配置文件。\n在项目的发布目录中创建 ExportOptions-MAS.plist：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE plist PUBLIC \u0026#34;-//Apple//DTD PLIST 1.0//EN\u0026#34; \u0026#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd\u0026#34;\u0026gt; \u0026lt;plist version=\u0026#34;1.0\u0026#34;\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;method\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;app-store\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;destination\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;upload\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;signingStyle\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;automatic\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;teamID\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;ABCDE12345\u0026lt;/string\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/plist\u0026gt; 各键的含义如下：\nmethod = app-store — 表示将此构建导出为 Mac App Store 渠道用途，与直接分发渠道的 developer-id 形成对比。 destination = upload — xcodebuild -exportArchive 会将导出的产物直接上传到 App Store Connect，无需像以前那样经过 Transporter 或其他上传工具。 signingStyle = automatic — Xcode 会自动匹配 Mac App Store 配置文件和 Apple Distribution 证书。 teamID — 你的 Apple Developer Team ID。 创建此文件一次后，即可在所有 MAS 发布中复用。保留直接分发用的 ExportOptions.plist 不变，将此文件与其并排放置。\n第 2 步 — 在 App Store Connect 注册应用记录 要实际提交应用审核，App Store Connect 中必须有应用记录 (record)。记录是容纳应用在应用商店中展示的所有信息的容器，包括应用名称、描述、截图、价格等。\n应用记录可以在实际首次提交前再创建。在一次性准备阶段，只需提前了解\u0026quot;需要确定哪些项目、如何确定\u0026quot;即可。特别是下面的 Primary Language，一旦设定便难以更改，请提前慎重决定。\n在 App Store Connect → My Apps → + → New App 中填写以下内容：\nPlatform — 选择 macOS Primary Language（主要语言） — ⚠️ 最需要慎重决定的项目。 一旦设定，通过自助服务更改非常困难。若计划在多个国家发布，通常将**英语（英语，美国）**设为主要语言，因为主要语言是\u0026quot;特定国家没有对应翻译时显示的基准语言\u0026quot;。 App Name（应用名称） — 以主要语言为基准的名称（如 FocusTimer）。其他语言的名称稍后通过各语言本地化 (localization) 单独添加。例如，可以让韩语用户看到韩语名称，日语用户看到日语名称。 Bundle ID — 从下拉菜单中选择 com.example.FocusTimer.mas。这正是第 1 篇中注册的那个 ID。请不要与直接分发用 Bundle ID（com.example.FocusTimer）混淆——必须选择带有 .mas 后缀的那个。 SKU — 仅供你自己使用的内部标识字符串，不会在应用商店中公开，可自由设定（如 focustimer-mas-001）。 应用分类必须与第 2 篇中在 Info.plist 的 LSApplicationCategoryType 中填写的值一致。若在 plist 中填写了 public.app-category.productivity（效率），则在 App Store Connect 中也必须选择\u0026quot;效率\u0026quot;类，否则审核阶段会出现不匹配警告。\n第 3 步 — 验证两个渠道的构建 现在同一代码库拥有了两个构建目标。这伴随着维护成本。平时只构建直接分发渠道，MAS 目标可能在不知不觉间已经损坏。例如，新增代码时遗漏了 #if canImport(Sparkle) 分支，导致只有 MAS 构建编译失败。\n防止这种情况最可靠的方法是每次制作直接分发发布时，同时也对 MAS 目标进行归档。若构建成功，则说明两个渠道的分支完好无损。\nxcodebuild -project FocusTimer.xcodeproj -scheme \u0026#34;FocusTimer MAS\u0026#34; \\ -configuration Release \\ -destination \u0026#39;platform=macOS\u0026#39; \\ -archivePath build/FocusTimer-MAS.xcarchive \\ archive 若输出末尾显示如下内容，则 MAS 分支正常：\n** ARCHIVE SUCCEEDED ** 建议将此命令嵌入发布自动化脚本的最后一步，使其在每次发布时自动运行。即使当时并未实际将 MAS 构建上传到 App Store，仅确认归档是否成功就足以保证两个渠道的分支没有被破坏。\n系列小结 — MAS 发布一次性准备完成 历经三篇的 Mac App Store 发布一次性准备工作全部完成。现在你已经准备好以下内容：\n✅ (第 1 篇) MAS 专用 Bundle ID + 复制的 FocusTimer MAS 构建目标 ✅ (第 2 篇) MAS 专用 entitlements 和 Info.plist + 构建设置 + #if canImport(Sparkle) 代码分支 ✅ (第 3 篇) 用于上传的 ExportOptions-MAS.plist + App Store Connect 记录注册方法 + 两个渠道构建验证 至此，同一个 macOS 应用同时具备了直接分发（Developer ID）和 Mac App Store 两个渠道。核心结构再次总结如下：\n共享同一代码库，但分为两个构建目标。 直接分发目标包含 Sparkle，MAS 目标排除 Sparkle。 两者的差异通过独立的 entitlements、Info.plist、ExportOptions 文件和 #if canImport(Sparkle) 代码分支来体现。 初次配置时工作量较大，但这同样是一次搭建、持续复用的结构。之后只需在制作直接分发发布时同时归档 MAS 构建，确认分支是否仍然正常即可。实际提交 App Store（截图、描述、应对审核）是在此一次性准备之上进行的独立工作，将在另一篇文章中介绍。\n参考资料 App Store Connect 帮助 LSApplicationCategoryType — Apple Developer Documentation Distributing your app for beta testing and releases — Apple Developer ","permalink":"https://hobbyworker.me/zh-hans/dev/2026-05-19-distribute-macos-app-mas-3-export-and-app-store-connect/","summary":"\u003ch1 id=\"将构建发送到-app-store-的路径\"\u003e将构建发送到 App Store 的路径\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"../2026-05-17-distribute-macos-app-mas-1-target-setup/\"\u003e第 1 篇\u003c/a\u003e创建了 MAS 构建目标，\u003ca href=\"../2026-05-18-distribute-macos-app-mas-2-build-config-and-code/\"\u003e第 2 篇\u003c/a\u003e创建了区分两个渠道的配置文件和代码分支。\u003ccode\u003eFocusTimer MAS\u003c/code\u003e 目标现在已成为可以上架 App Store 的形态。\u003c/p\u003e\n\u003cp\u003e本篇（最终篇）将搭建\u003cstrong\u003e将该构建上传到 App Store Connect 的路径\u003c/strong\u003e，并介绍如何验证两个渠道在未来不会被破坏，从而收尾本系列。\u003c/p\u003e","title":"macOS 应用上架 Mac App Store (3): 上传配置与 App Store Connect 注册"},{"content":"让目标真正成为\u0026quot;MAS 专用\u0026quot; 在第 1 篇中，我们注册了 MAS 专用 Bundle ID 并复制了 FocusTimer MAS 构建目标。但该目标目前仍只是直接分发目标的副本。\nMAS 构建必须与直接分发构建在三个方面有所不同：\n权限 (entitlements) — 仅保留适合 App Store 的最小权限 Info.plist — 移除 Sparkle 键，添加 App Store 元数据 代码 — 分支处理，确保没有 Sparkle 时也能编译 本文将逐一处理这三点。\n与第 1 篇相同，FocusTimer、com.example.FocusTimer.mas 等均为示例值。\n第 1 步 — MAS 专用 entitlements 文件 entitlements 是列出应用请求哪些系统权限的文件。直接分发构建中，Sparkle 为了安装更新需要额外权限；但 MAS 构建没有 Sparkle，因此那些权限是不必要的。\n在项目根目录创建新文件 FocusTimer-MAS.entitlements：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE plist PUBLIC \u0026#34;-//Apple//DTD PLIST 1.0//EN\u0026#34; \u0026#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd\u0026#34;\u0026gt; \u0026lt;plist version=\u0026#34;1.0\u0026#34;\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;com.apple.security.app-sandbox\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;com.apple.security.files.user-selected.read-write\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/plist\u0026gt; 与直接分发用 FocusTimer.entitlements 相比，差异如下：\n无 Sparkle 相关权限 — 直接分发构建中的 temporary-exception.mach-lookup.*（Sparkle 与安装助手通信所需的例外）在 MAS 构建中没有存在的理由。 无 network.client — 移除 Sparkle 后，FocusTimer 本体不进行任何网络调用。不使用的权限理应移除，审核中多余权限也可能被指出。应用请求的权限面越小越好。 上述示例是仅拥有\u0026quot;读写用户主动选择的文件\u0026quot;权限的最简形式。若你的应用实际使用了网络或其他资源，请相应添加对应权限，但绝对不要包含不使用的权限。\n第 2 步 — MAS 专用 Info.plist 文件 同样，在项目根目录创建新文件 FocusTimer-MAS-Info.plist：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE plist PUBLIC \u0026#34;-//Apple//DTD PLIST 1.0//EN\u0026#34; \u0026#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd\u0026#34;\u0026gt; \u0026lt;plist version=\u0026#34;1.0\u0026#34;\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;LSApplicationCategoryType\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;public.app-category.productivity\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;ITSAppUsesNonExemptEncryption\u0026lt;/key\u0026gt; \u0026lt;false/\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/plist\u0026gt; 与直接分发用 FocusTimer-Info.plist 相比，有两点不同：\n① 去掉所有 Sparkle 的 SU* 键\n直接分发用 Info.plist 中包含 SUFeedURL、SUPublicEDKey 等 Sparkle 配置键。MAS 用 Info.plist 中一个都不应有。App Store 审核禁止自带自动更新机制，即使 plist 中仅残留暗示此功能的键也可能成为问题。不包含这些键才是安全的做法。\n② 添加 App Store 元数据键\nLSApplicationCategoryType — 应用在 App Store 中所属的分类。上述示例中的 public.app-category.productivity 表示\u0026quot;效率\u0026quot;分类。该值必须与第 3 篇中在 App Store Connect 设置的分类一致，否则审核阶段会出现不匹配警告。 ITSAppUsesNonExemptEncryption — 应用是否使用受出口管制的加密技术。若仅使用标准加密（如 HTTPS、标准系统 API）或不使用加密，可设为 false。提前硬编码此键，可自动通过每次上传构建时出现的加密调查问卷。 将 ITSAppUsesNonExemptEncryption 设为 false 之前，请确认你的应用确实未使用非标准加密。若不确定，建议参考 Apple 的加密相关文档。\n第 3 步 — MAS 目标构建设置 这是第 1 篇 2-3 节中推迟的构建设置更新。在 TARGETS → FocusTimer MAS → Build Settings 中对齐以下设置：\n构建设置键 值 PRODUCT_BUNDLE_IDENTIFIER com.example.FocusTimer.mas INFOPLIST_FILE FocusTimer-MAS-Info.plist CODE_SIGN_ENTITLEMENTS FocusTimer-MAS.entitlements ENABLE_APP_SANDBOX YES ENABLE_HARDENED_RUNTIME YES ENABLE_USER_SELECTED_FILES readwrite CODE_SIGN_STYLE Automatic MARKETING_VERSION、CURRENT_PROJECT_VERSION 与直接分发目标保持一致 核心要点如下：\nINFOPLIST_FILE 和 CODE_SIGN_ENTITLEMENTS 必须指向刚才创建的 MAS 专用文件。常见的错误是将这两行保持为直接分发用文件不变。 将 CODE_SIGN_STYLE 设为 Automatic，Xcode 会自动发放和匹配 Mac App Store 配置文件和 Apple Distribution 证书，无需手动管理签名。 若应用不使用网络，则不设置 ENABLE_OUTGOING_NETWORK_CONNECTIONS，这与在 entitlements 中移除 network.client 的逻辑一致。 版本号（MARKETING_VERSION 等）保持与直接分发目标相同的值，以免两个渠道的版本号出现差异。 第 4 步 — Sparkle 代码分支（#if canImport(Sparkle)） 即使拆分了配置文件，还有一个问题未解决：源代码中某处有 import Sparkle 及使用 Sparkle 的代码，而 MAS 目标中未链接 Sparkle，因此 import 语句本身会产生编译错误。\n解决方案是用 Swift 的条件编译指令 #if canImport(Sparkle) 包裹 Sparkle 相关代码。\n为何用 canImport — 不使用自定义标志的原因 也可以用 #if MAS_BUILD 等自定义编译标志来分支。但这样就需要人工同步\u0026quot;MAS 目标开启该标志，直接分发目标关闭该标志\u0026quot;的设置。随着目标增多或设置变化，容易出现不一致。\ncanImport(Sparkle) 则不同。它由编译器直接检查\u0026quot;Sparkle.framework 是否链接到此目标\u0026quot;。由于第 1 篇中已从 MAS 目标的包依赖中删除了 Sparkle，在 MAS 目标中 canImport(Sparkle) 会自动为假 (false)，无需单独同步任何标志。这就是 macOS 应用分离 MAS 渠道时将此模式作为事实标准的原因。\n分支 ① — 完全包裹仅含 Sparkle 代码的文件 仅包含自动更新逻辑的文件（如 UpdaterCoordinator.swift）整体用 #if 包裹：\n#if canImport(Sparkle) import Foundation import Sparkle import Combine @Observable @MainActor final class UpdaterCoordinator { // 包裹 Sparkle 更新器的代码 … } #endif 在 MAS 构建中，此文件会像空文件一样编译，不产生任何影响。\n分支 ② — 同时包裹使用 Sparkle 的代码 创建 UpdaterCoordinator 并将其传递给界面的代码也需要分支。否则 MAS 构建会引用\u0026quot;不存在的类型\u0026quot;。\n@main struct FocusTimerApp: App { @State private var viewModel = CompositionRoot.makeContentViewModel() #if canImport(Sparkle) @State private var updater = UpdaterCoordinator() #endif var body: some Scene { Settings { #if canImport(Sparkle) PreferenceView(viewModel: viewModel, updater: updater) #else PreferenceView(viewModel: viewModel) #endif } MenuBarExtra { #if canImport(Sparkle) MenuBarContent(updater: updater) #else MenuBarContent() #endif } label: { // 菜单栏图标 … } } } private struct MenuBarContent: View { #if canImport(Sparkle) @Bindable var updater: UpdaterCoordinator #endif var body: some View { Button(\u0026#34;显示 FocusTimer\u0026#34;) { /* … */ } #if canImport(Sparkle) Button(\u0026#34;检查更新…\u0026#34;) { updater.checkForUpdates() } .disabled(!updater.canCheckForUpdates) #endif // 其他共用菜单项 … } } 要点有三：\nupdater 字段声明本身用 #if 包裹。 接收 updater 的界面（PreferenceView、MenuBarContent）分为有 Sparkle 和无 Sparkle 两种形式。 \u0026ldquo;检查更新…\u0026ldquo;等 Sparkle 专用 UI 元素也用 #if 包裹。在 MAS 构建中，这些菜单项不会出现——App Store 版不应有自带更新菜单，这是正确的。 设置界面（PreferenceView）中的\u0026quot;启用自动更新\u0026quot;开关等元素也都以相同的模式包裹即可。\n第 2 篇小结 跟到这里，你现在已经准备好以下内容：\n✅ MAS 专用权限文件 FocusTimer-MAS.entitlements（最小权限） ✅ MAS 专用 FocusTimer-MAS-Info.plist（移除 Sparkle 键，添加 App Store 元数据） ✅ MAS 目标构建设置已对齐 ✅ 用 #if canImport(Sparkle) 完成了自动更新代码的分支 现在，FocusTimer MAS 目标已真正有别于直接分发目标，成为可以上架 App Store 的形态。剩下的工作是搭建将此构建实际上传到 App Store Connect 的路径。\n下一篇将创建用于上传的 ExportOptions-MAS.plist、在 App Store Connect 中注册应用记录，并介绍如何验证两个渠道在未来不会被破坏，从而收尾本系列。\n","permalink":"https://hobbyworker.me/zh-hans/dev/2026-05-18-distribute-macos-app-mas-2-build-config-and-code/","summary":"\u003ch1 id=\"让目标真正成为mas-专用\"\u003e让目标真正成为\u0026quot;MAS 专用\u0026quot;\u003c/h1\u003e\n\u003cp\u003e在\u003ca href=\"../2026-05-17-distribute-macos-app-mas-1-target-setup/\"\u003e第 1 篇\u003c/a\u003e中，我们注册了 MAS 专用 Bundle ID 并复制了 \u003ccode\u003eFocusTimer MAS\u003c/code\u003e 构建目标。但该目标目前仍只是直接分发目标的副本。\u003c/p\u003e\n\u003cp\u003eMAS 构建必须与直接分发构建在三个方面有所不同：\u003c/p\u003e","title":"macOS 应用上架 Mac App Store (2): 区分两个渠道的配置与代码分支"},{"content":"另一个分发渠道 — Mac App Store 前一系列介绍了用 Developer ID 直接分发 macOS 应用的一次性准备工作。配齐证书、公证、Sparkle 自动更新和更新 Feed 托管后，用户无需经过 App Store 即可直接下载 .dmg 文件。\n本系列介绍将同一应用上架 Mac App Store (MAS) 的一次性准备工作。两种分发方式并非二选一，可以同时运营直接分发渠道和 App Store 渠道。App Store 由 Apple 代劳处理支付、退款和搜索曝光，用户信任度也更高，因此与直接分发并行运营的情况很常见。\n本系列以应用已通过 Developer ID 直接分发、且已集成 Sparkle 自动更新为前提。该配置从 Developer ID 直接分发系列第 1 篇开始介绍。若尚未阅读，建议先行查阅。\n为何 MAS 需要\u0026quot;另一个目标\u0026quot; 直接分发的应用包含 Sparkle 自动更新——应用自行检查更新 Feed，下载新版本并替换自身的功能。\n然而，Mac App Store 的审核规定禁止此类行为。上架 App Store 的应用不得从外部下载代码更新自身，更新必须且只能通过 App Store 进行。也就是说，提交 MAS 的构建中不能包含 Sparkle。\n但直接分发构建必须包含 Sparkle。同一构建产物无法同时满足两种要求。因此，解决方案是：\n同一代码库，两个构建目标。\n渠道 构建目标 Sparkle 分发途径 Developer ID FocusTimer 包含 直接分发（.dmg） Mac App Store FocusTimer MAS 排除 App Store 共享同一份源代码，将构建目标分为两个，只在其中一个中链接 Sparkle。本系列介绍创建第二个目标（FocusTimer MAS）的一次性准备工作。\n本系列将完成的内容 共三篇，逐步搭建以下内容：\n(第 1 篇，本文) 注册 MAS 专用 Bundle ID + 复制 Xcode 构建目标 (第 2 篇) 区分两个渠道的配置文件（entitlements 和 Info.plist）与代码分支 (第 3 篇) 用于上传的 ExportOptions-MAS.plist + App Store Connect 注册 + 构建验证 系列结束时，你应该已经准备好以下内容：\n在 Apple Developer Portal 注册的 MAS 专用 Bundle ID 已移除 Sparkle 依赖的 FocusTimer MAS 构建目标 MAS 专用 entitlements 文件和 Info.plist 文件 用 #if canImport(Sparkle) 分支的代码 用于 App Store 上传的 ExportOptions-MAS.plist 示例应用 — FocusTimer 与前一系列相同，以虚构的 macOS 应用 FocusTimer（管理专注时间的简单计时器应用）为例。FocusTimer、Bundle Identifier com.example.FocusTimer、Team ID ABCDE12345 等均为示例值，实际使用时请替换为你自己的应用和账户信息。\n本文基于 Xcode 26 编写。\n第 1 步 — 注册 MAS 专用 Bundle ID 为何需要独立的 Bundle ID 若直接分发渠道的 Bundle ID 为 com.example.FocusTimer，则 MAS 渠道使用不同的 Bundle ID。\n渠道 Bundle ID Developer ID com.example.FocusTimer Mac App Store com.example.FocusTimer.mas 在原有 Bundle ID 后追加 .mas。为何要分开？\n同一台 Mac 上两个渠道可以共存 — Bundle ID 相同时，macOS 会将两个应用识别为同一个并产生冲突。若不同，直接分发版和 App Store 版可以同时安装在同一台机器上（便于开发和测试）。 UserDefaults 域名隔离 — 设置存储空间以 Bundle ID 为单位划分，两个渠道的设置不会混淆。 避免 Launch Services 冲突 — 防止 macOS 在处理\u0026quot;打开此应用\u0026quot;等请求时产生的混乱。 请保持直接分发渠道的 Bundle ID 不变。这是识别现有用户的基准，不能更改。MAS 用的是额外注册的一个新 ID。\n注册流程 登录 developer.apple.com/account（使用 Apple Developer Program 账户） Certificates, Identifiers \u0026amp; Profiles → 左侧菜单 Identifiers → + 按钮 选择 App IDs → Continue → 类型选择 App → Continue Description：输入易于识别的名称（如 FocusTimer MAS） Bundle ID： 选择 Explicit（明确） 输入：com.example.FocusTimer.mas Capabilities：关闭应用不使用的所有功能。若不使用 iCloud、推送通知、应用内购买、Sign in with Apple 等，无需开启任何项目。（App Sandbox 由 Xcode 侧的 entitlements 处理，此处无需开启。） Continue → Register 验证 若 Identifiers 列表中显示 FocusTimer MAS — com.example.FocusTimer.mas 条目，则注册完成。\n第 2 步 — 复制 Xcode 构建目标 现在在 Xcode 项目中创建 FocusTimer MAS 构建目标。最快的方法是复制现有目标。\n2-1. 复制 (Duplicate) 目标 在 Xcode 中打开 FocusTimer.xcodeproj 点击 Project navigator（左侧面板）最上方的项目图标（蓝色） 在中间编辑区域的 TARGETS 列表中右键点击 FocusTimer → Duplicate 弹出对话框时选择 Duplicate Only 新目标名称为 FocusTimer copy，双击名称将其改为 FocusTimer MAS 若弹出\u0026quot;是否添加新 Scheme？\u0026ldquo;提示，选择 Activate（或稍后在 Manage Schemes 中添加） 2-2. 立即修改 Bundle ID 复制后最先要做的是替换 Bundle ID。若保持不变，两个目标将拥有相同的 ID。\n将 TARGETS → FocusTimer MAS → General → Identity → Bundle Identifier 改为第 1 步中注册的值：\ncom.example.FocusTimer.mas 2-3. 关键陷阱 — Duplicate 留下的\u0026quot;清理负担\u0026rdquo; 这是本篇中最棘手的部分。Xcode 的 Duplicate Target 以**\u0026ldquo;安全默认值\u0026rdquo;**运行，但这种安全反而留下了需要手动处理的清理工作。复制完成后需要检查以下四点：\n① 让新目标包含源文件\n为了安全，Duplicate 会自动将所有源文件从新目标中排除。若保持不变，FocusTimer MAS 目标就是一个空壳，构建时不会包含任何代码。需要重新整理目标成员关系，使新目标重新包含现有源文件。\n② 从 MAS 目标中移除 Sparkle 依赖\nDuplicate 会原样复制原始目标的 Swift Package 依赖。因此 Sparkle 也会随之进入 FocusTimer MAS 目标。本系列的出发点正是\u0026quot;MAS 构建中不能有 Sparkle\u0026quot;，所以需要从 MAS 目标的包依赖列表和 Frameworks 构建阶段中删除 Sparkle。\n③ 清理临时 Info.plist 文件\nDuplicate 会创建类似 FocusTimer copy-Info.plist 的临时 Info.plist 文件。由于我们将在第 2 篇中单独创建 MAS 专用的 Info.plist，这些临时文件（包括项目引用和实际文件）应全部删除。\n④ 更新 MAS 目标的构建设置\nBundle Identifier、Info.plist 路径、entitlements 路径等构建设置需要调整为 MAS 专用。此工作将在第 2 篇创建专用配置文件后统一处理。\n①·② 大多可通过 Xcode UI 中的目标成员关系和包依赖界面处理，但若复制过程留下的重复引用无法彻底清除，有时需要直接查看项目文件（.pbxproj）。清理完成后，分别构建两个目标，确认 MAS 目标构建中 import Sparkle 是否报错，是最可靠的验证方式（第 2 篇中介绍此代码分支）。\n第 1 篇小结 跟到这里，你现在已经准备好以下内容：\n✅ 在 Apple Developer Portal 注册了 MAS 专用 Bundle ID（com.example.FocusTimer.mas） ✅ 通过复制创建了 FocusTimer MAS 构建目标 ✅ 了解了复制留下的清理负担（源文件成员关系、Sparkle 依赖、临时文件） 目标这个\u0026quot;容器\u0026quot;已经创建好了。但这个目标目前本质上还只是 FocusTimer 的副本，并未真正成为\u0026quot;MAS 专用\u0026quot;。MAS 构建需要使用与直接分发构建不同的权限 (entitlements) 和不同的 Info.plist，代码也需要分支以便在没有 Sparkle 时不报错。\n下一篇将介绍区分两个渠道的配置文件与代码分支。\n","permalink":"https://hobbyworker.me/zh-hans/dev/2026-05-17-distribute-macos-app-mas-1-target-setup/","summary":"\u003ch1 id=\"另一个分发渠道--mac-app-store\"\u003e另一个分发渠道 — Mac App Store\u003c/h1\u003e\n\u003cp\u003e前一系列介绍了用 \u003cstrong\u003eDeveloper ID 直接分发\u003c/strong\u003e macOS 应用的一次性准备工作。配齐证书、公证、Sparkle 自动更新和更新 Feed 托管后，用户无需经过 App Store 即可直接下载 \u003ccode\u003e.dmg\u003c/code\u003e 文件。\u003c/p\u003e","title":"macOS 应用上架 Mac App Store (1): 创建独立构建目标"},{"content":"最后一块拼图 — 更新文件放在哪里 第 1 篇完成了 Developer ID 证书和公证的配置，第 2 篇完成了 Sparkle 签名密钥的准备。至此，我们已具备签名应用、公证应用，以及验证更新真实性的全部手段。\n然而，第 2 篇中写入应用 Info.plist 的 SUFeedURL（https://updates.example.com/appcast.xml）所指向的位置，目前还什么都没有。本篇（最终篇）将托管更新 Feed，并完成构建配置，收尾整个一次性准备工作。\n与第 1、2 篇相同，所有名称和域名（FocusTimer、example.com、example-dev 等）均为示例值，实际使用时请替换为你自己的信息。\n为何要单独建立更新仓库 自动更新要正常运作，互联网上必须有以下两样内容：\nappcast.xml — 更新 Feed，告知应用哪个版本是最新的以及从哪里获取 .dmg — 实际的应用安装文件 这里有一个重要限制：应用内的 Sparkle 通过简单的、无认证的 HTTPS GET 请求获取这些文件。也就是说，用户电脑上的应用必须能够不经任何登录等验证流程直接下载。\n很多开发者将应用的主源码仓库设为私有 (private)。然而私有仓库的发布文件需要认证，Sparkle 无法获取。因此，常见的做法是分离仓库：\n主仓库（如 FocusTimer）— 源代码，可设为私有。 更新仓库（如 FocusTimer-updates）— 仅托管 appcast.xml，必须为公开 (public)。 本文将使用 GitHub 免费的静态托管服务 GitHub Pages 运营更新仓库。\n第 1 步 — 创建公开更新仓库 在 GitHub 上创建新仓库。\n点击 New repository 名称：FocusTimer-updates — 该名称稍后会用于 Feed 地址，请精确设置，包括大小写。 所有者 (Owner)：你的账户或组织（示例：example-dev） 可见性：Public — 必须为公开，Sparkle 需要无认证即可访问。 勾选 Add a README file（方便创建第一个提交） 点击 Create repository 第 2 步 — 启用 GitHub Pages 将刚创建的仓库作为静态网站发布。\n进入仓库的 Settings → 左侧菜单选择 Pages Source：选择 Deploy from a branch Branch：选择 main / (root) → 点击 Save 等待 1~2 分钟，若 https://example-dev.github.io/FocusTimer-updates/ 可以访问，则表示成功。 至此，已有了一个可以存放更新文件的公开地址。但还有最后一步。\n第 3 步 — 连接自定义域名 直接使用 GitHub Pages 的默认地址（example-dev.github.io/...）也可以正常工作。但如果将该地址硬编码到应用的 SUFeedURL 中，日后若需要将托管迁移到 GitHub Pages 以外的地方，就会遇到麻烦——因为所有已分发用户的应用仍指向旧地址。\n解决方案是中间加一层自己控制的域名。将 SUFeedURL 设为 https://updates.example.com/appcast.xml 等自己的域名后，日后迁移托管时只需修改一行 DNS 配置，现有用户即可自动跟随到新位置。这个配置只需做一次，永久有效。\n3-1. 添加 DNS 记录 在管理域名（example.com）的 DNS 服务商的配置界面中，添加以下记录：\n字段 值 Type CNAME Name updates Value example-dev.github.io TTL 3600（默认值） 这样，updates.example.com 子域名就会指向 GitHub Pages。\n3-2. 在仓库中添加 CNAME 文件 在更新仓库（FocusTimer-updates）的根目录创建名为 CNAME 的文件，内容只写一行域名：\nupdates.example.com 提交并推送此文件。\n3-3. 在 GitHub Pages 中注册域名 在仓库的 Settings → Pages → Custom domain 字段中输入 updates.example.com 并点击 Save。GitHub 会自动颁发 HTTPS 证书，证书颁发完成后勾选 Enforce HTTPS。\n3-4. 验证 DNS 传播和证书颁发通常需要约 10 分钟。稍等片刻后用以下命令验证：\ncurl -I https://updates.example.com/appcast.xml 若响应第一行显示 HTTP/2 200，则表示正常。（若尚未上传 appcast.xml，可能会返回 404，但域名和 HTTPS 连接本身可通过其他方式确认。关键是 https://updates.example.com 能够返回响应。）\n补充说明：.dmg 安装文件通常上传到 GitHub Releases，而非 GitHub Pages。直接将大型二进制文件放入仓库会使仓库体积膨胀。Releases 的服务路径与 Pages 不同，因此与上述自定义域名配置无关，保持现状即可。\n第 4 步 — 在本地保存更新仓库 发布时需要编辑并提交 appcast.xml，因此要在本地也克隆一份更新仓库。将其克隆到主项目文件夹内的合适位置（如 release/updates），构建和发布脚本就能在同一个地方操作两个仓库，非常方便。\ngit clone git@github.com:example-dev/FocusTimer-updates.git release/updates 将一个仓库嵌套在另一个仓库中时，请将 release/updates/ 添加到主仓库的 .gitignore 中，以避免相互干扰。\n第 5 步 — ExportOptions.plist 接下来是构建侧的配置。Xcode 的 xcodebuild -exportArchive 命令在将归档导出为分发用 .app 时，会读取名为 ExportOptions.plist 的配置文件。直接分发（Developer ID 渠道）的内容如下：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE plist PUBLIC \u0026#34;-//Apple//DTD PLIST 1.0//EN\u0026#34; \u0026#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd\u0026#34;\u0026gt; \u0026lt;plist version=\u0026#34;1.0\u0026#34;\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;method\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;developer-id\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;signingStyle\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;automatic\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;teamID\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;ABCDE12345\u0026lt;/string\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/plist\u0026gt; method — developer-id，表示\u0026quot;直接分发，而非 Mac App Store\u0026quot;。 signingStyle — automatic，让 Xcode 自动选用第 1 篇中申请的证书。 teamID — 第 1 篇中记下的 Team ID。 在项目中创建此文件一次（如 release/ExportOptions.plist），即可在所有后续发布中复用。\n第 6 步 — 检查应用侧配置 最后整理一下应用项目本身必须包含的配置。首次为新应用集成 Sparkle 时，可将此列表作为检查清单使用。\nInfo.plist 这些是第 2 篇中添加的 Sparkle 键。Info.plist 必须包含以下键：\n\u0026lt;key\u0026gt;SUFeedURL\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;https://updates.example.com/appcast.xml\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;SUPublicEDKey\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=\u0026lt;/string\u0026gt; 请再次确认 SUFeedURL 指向第 3 步创建的自定义域名，且 SUPublicEDKey 与第 2 篇中生成的公钥一致。\n权限 (Entitlements) 若应用使用 App Sandbox，Sparkle 需要额外的例外权限以便与内部服务通信来安装更新。将以下条目添加到 FocusTimer.entitlements 文件中：\n\u0026lt;key\u0026gt;com.apple.security.app-sandbox\u0026lt;/key\u0026gt;\u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;com.apple.security.network.client\u0026lt;/key\u0026gt;\u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;com.apple.security.temporary-exception.mach-lookup.global-name\u0026lt;/key\u0026gt; \u0026lt;array\u0026gt; \u0026lt;string\u0026gt;$(PRODUCT_BUNDLE_IDENTIFIER)-spks\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;$(PRODUCT_BUNDLE_IDENTIFIER)-spki\u0026lt;/string\u0026gt; \u0026lt;/array\u0026gt; 构建时，Xcode 会将 $(PRODUCT_BUNDLE_IDENTIFIER) 自动替换为实际的 Bundle Identifier（com.example.FocusTimer）。详细条目请参考 Sparkle 官方沙盒指南。\n对于直接分发的应用，沙盒并非必须（沙盒是 Mac App Store 的要求）。若应用不使用沙盒，则无需上述 mach-lookup 例外。但由于自动更新需要使用网络，network.client 权限和 Hardened Runtime 必须开启，以满足公证要求。\n构建设置 (Build Settings) 在 Xcode 的构建设置中确认以下项目：\nCODE_SIGN_ENTITLEMENTS — 上述权限文件的路径 ENABLE_HARDENED_RUNTIME = YES — 公证的必要条件 ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES — 允许用于检查更新的网络访问 系列小结 — 一次性准备工作完成 历经三篇的直接分发一次性准备工作全部完成。现在你已经准备好以下内容：\n✅ (第 1 篇) Developer ID Application 证书 + 公证用凭据 ✅ (第 2 篇) Sparkle EdDSA 签名密钥对 + 私钥备份 ✅ (第 3 篇) 连接到自定义域名的公开更新仓库 + ExportOptions.plist + 应用侧配置 以上是只需做一次的配置。这些工作不需要在每次发布新版本时重复进行。\n从现在起，分发新版本的流程每次大致相同——构建归档 → 用 ExportOptions.plist 导出 → 用 Developer ID 证书签名 → 用 notarytool 公证 → 创建 .dmg → 用 Sparkle 密钥签名 → 更新 appcast.xml → 将 .dmg 上传到 GitHub Release。这一重复流程大多可以用一个脚本自动化，那是另一篇文章的主题。\n其中**制作 .dmg**涉及背景图片和图标排列等设计要素，将在独立系列 macOS 应用分发用 DMG 设计 中详细介绍。\n直接分发初次配置时看似繁琐，但核心在于**\u0026ldquo;一旦搭建好，便可持续复用\u0026rdquo;**。放弃 App Store 的部分便利，换来的是对整个分发流程的完全掌控。\n参考资料 Sparkle 官方文档 Apple — Notarizing macOS software before distribution GitHub Pages 文档 ","permalink":"https://hobbyworker.me/zh-hans/dev/2026-05-16-distribute-macos-app-3-update-hosting-and-build/","summary":"\u003ch1 id=\"最后一块拼图--更新文件放在哪里\"\u003e最后一块拼图 — 更新文件放在哪里\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"../2026-05-14-distribute-macos-app-1-developer-id-certificate/\"\u003e第 1 篇\u003c/a\u003e完成了 Developer ID 证书和公证的配置，\u003ca href=\"../2026-05-15-distribute-macos-app-2-sparkle-signing-key/\"\u003e第 2 篇\u003c/a\u003e完成了 Sparkle 签名密钥的准备。至此，我们已具备签名应用、公证应用，以及验证更新真实性的全部手段。\u003c/p\u003e","title":"macOS 应用自主分发 (3): 托管更新 Feed 与构建配置"},{"content":"自动更新，以及为何还需要额外一层签名 在第 1 篇中，我们完成了 Developer ID 证书和公证的配置。至此，应用已具备首次交付给用户的条件。但应用并非发布一次就结束——你需要持续推出修复 Bug、增加功能的新版本。\n对于 Mac App Store 应用，更新由 App Store 全权处理。直接分发的应用则不然，你必须在应用内自行集成自动更新功能。在 macOS 上，承担这一角色的事实标准是开源框架 Sparkle。集成 Sparkle 后，应用会定期检查\u0026quot;更新 Feed (appcast)\u0026quot;，如有新版本则通知用户并自动下载安装。\n这里产生了一个疑问：第 1 篇已经用 Developer ID 证书对应用进行了签名，为什么还需要另一把密钥？\n原因在于两种签名验证的对象不同：\nDeveloper ID 证书 — macOS Gatekeeper 用于判断\u0026quot;是否允许安装此应用\u0026quot; Sparkle EdDSA 密钥 — 应用内的 Sparkle 用于判断\u0026quot;刚下载的更新文件是否真的由该应用的开发者制作\u0026quot; 自动更新是一种安全敏感操作——应用从互联网下载文件并覆盖自身。如果有人拦截更新服务器或通信路径并植入伪造文件，后果将非常严重。为此，Sparkle 只接受用开发者独有的私钥签名的更新，签名不匹配则拒绝安装。这是独立于证书之外的额外验证层。\n本文将创建用于该验证的 EdDSA (Ed25519) 密钥对。\n与第 1 篇相同，所有名称和路径（FocusTimer、example.com 等）均为示例值，实际使用时请替换为你自己的应用信息。\n前提 — 应用中必须已添加 Sparkle 创建密钥之前，应用项目中必须已将 Sparkle 框架添加为依赖项。若尚未添加，请在 Xcode 中通过 Swift Package Manager (SPM) 添加。\n在 Xcode 中打开项目 → File → Add Package Dependencies… 在搜索框中输入仓库地址：https://github.com/sparkle-project/Sparkle 将版本规则设为 2.x（最新主版本）并添加 完成后构建一次项目，SPM 会下载 Sparkle 包。随包附带的命令行工具是下一步的关键。\n第 1 步 — 找到 Sparkle 命令行工具 Sparkle 包内附有用于密钥生成和签名的命令行工具。这些工具位于 SPM 下载包的文件夹中，但其路径因 Xcode 版本和 DerivedData 设置而异，因此直接查找更为稳妥。\nSPARKLE_BIN=$(find ~/Library/Developer/Xcode/DerivedData \\ -path \u0026#34;*/artifacts/sparkle/Sparkle/bin\u0026#34; -type d 2\u0026gt;/dev/null | head -1) echo \u0026#34;$SPARKLE_BIN\u0026#34; 若输出一行路径则表示成功。若无任何输出，说明跳过了前面\u0026quot;构建一次项目\u0026quot;的步骤，请先在 Xcode 中执行一次构建后再试。\n该文件夹内包含以下工具：\ngenerate_keys — 生成、备份、恢复和验证签名密钥（本文使用） sign_update — 对更新文件进行签名（实际发布时使用） generate_appcast — 生成更新 Feed（appcast.xml）（第 3 篇中出现） 第 2 步 — 生成签名密钥 现在创建密钥对。\n\u0026#34;$SPARKLE_BIN/generate_keys\u0026#34; 输出类似如下内容：\nGenerating a new signing key... A key has been generated and saved in your keychain. Add the SUPublicEDKey key to the Info.plist of each app for which you intend to use Sparkle... \u0026lt;key\u0026gt;SUPublicEDKey\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=\u0026lt;/string\u0026gt; 这一条命令会生成两个密钥：\n公钥 (public key) — 输出中显示的 SUPublicEDKey 值。这不是秘密，是需要嵌入应用的密钥。 私钥 (private key) — 不会显示在屏幕上。自动以\u0026quot;Private key for signing Sparkle updates\u0026quot;为名保存到 macOS 钥匙串中。这是绝对不能以明文文件形式留在磁盘上的真正秘密。 将公钥嵌入应用 将输出的公钥字符串写入应用的 Info.plist。以示例应用为例，在 FocusTimer-Info.plist 中添加以下键：\n\u0026lt;key\u0026gt;SUFeedURL\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;https://updates.example.com/appcast.xml\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;SUPublicEDKey\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=\u0026lt;/string\u0026gt; SUPublicEDKey — 刚生成的公钥。应用使用此密钥验证已下载更新的签名。 SUFeedURL — 更新 Feed 的地址。该域名目前尚不存在，将在第 3 篇中创建。现在只是先占个位置。 由于公钥嵌入在应用中，而私钥只有开发者持有，因此应用只会接受用私钥签名的更新。这就是 Sparkle 更新验证的核心结构。\n第 3 步 — 备份私钥（务必执行！） 跳过此步骤，日后可能追悔莫及。\n私钥存储在钥匙串中，在当前使用的电脑上没有问题。但如果电脑丢失、磁盘损坏或重装 macOS，密钥也会随之消失。\n私钥丢失会怎样？用新密钥签名的更新将被现有用户的应用（嵌入了旧公钥的应用）拒绝验证。也就是说，将永远无法向已在使用应用的用户推送自动更新，只能逐一通知用户\u0026quot;请手动下载新版本并重新安装\u0026quot;。\n因此，密钥创建后请立即备份。\n\u0026#34;$SPARKLE_BIN/generate_keys\u0026#34; -x ~/focustimer-sparkle-private.key cat ~/focustimer-sparkle-private.key cat 输出的单行 base64 字符串即为私钥，示例如下：\nHn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0= 将此字符串以安全备注 (secure note) 的形式保存到 1Password 等密码管理器中。 备注名称建议使用便于日后查找的名称，例如 FocusTimer Sparkle EdDSA Private Key。\n确认保存完毕后，立即删除磁盘上的明文文件。\nrm ~/focustimer-sparkle-private.key 原则是不将私钥以明文文件形式留在磁盘上。备份只保存在加密的密码管理器中。\n关键陷阱 — % 符号不是密钥的一部分 用 cat 输出密钥时，终端（尤其是 zsh）可能在行末附加 % 符号。\nHn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0=% 这个 % 只是 Shell 用于提示\u0026quot;输出结束时无换行符\u0026quot;的标记，不是密钥的一部分。如果将 % 一同复制到备份中，日后恢复时密钥会损坏。base64 字符串通常以 = 结尾，请在保存时去掉 = 后面的 %。\n第 4 步 — 在其他电脑上恢复 需要在新电脑上构建应用时，将备份的私钥重新导入钥匙串。\necho \u0026#34;备份的_base64_字符串\u0026#34; \u0026gt; ~/focustimer-sparkle-private.key \u0026#34;$SPARKLE_BIN/generate_keys\u0026#34; -f ~/focustimer-sparkle-private.key rm ~/focustimer-sparkle-private.key -f 选项的含义是\u0026quot;将文件中的密钥导入钥匙串\u0026quot;。恢复完成后，同样立即删除明文文件。\n第 5 步 — 验证 确认密钥是否已正确安装。\n\u0026#34;$SPARKLE_BIN/generate_keys\u0026#34; -p 输出单行公钥。此值必须与第 2 步中写入 Info.plist 的 SUPublicEDKey 完全一致。若不一致，说明应用中嵌入的公钥与实际签名密钥不匹配，更新验证将会失败。\n第 2 篇小结 跟到这里，你现在已经准备好以下内容：\n✅ 生成了 Sparkle EdDSA 密钥对（公钥 + 私钥） ✅ 将公钥嵌入应用的 Info.plist（SUPublicEDKey） ✅ 将私钥安全备份到密码管理器 ✅ 了解了在其他电脑上恢复密钥的方法 应用现在已具备验证\u0026quot;下载的更新是否真实\u0026quot;的手段。但有一件事仍未完成：第 2 篇中为 SUFeedURL 填写的 https://updates.example.com/appcast.xml，该地址目前还什么都没有。\n下一篇将创建用于存放更新 Feed（appcast.xml）和 .dmg 文件的公开仓库，将其连接到我们自己控制的域名，并完成构建配置，从而结束全部一次性准备工作。\n","permalink":"https://hobbyworker.me/zh-hans/dev/2026-05-15-distribute-macos-app-2-sparkle-signing-key/","summary":"\u003ch1 id=\"自动更新以及为何还需要额外一层签名\"\u003e自动更新，以及为何还需要额外一层签名\u003c/h1\u003e\n\u003cp\u003e在\u003ca href=\"../2026-05-14-distribute-macos-app-1-developer-id-certificate/\"\u003e第 1 篇\u003c/a\u003e中，我们完成了 Developer ID 证书和公证的配置。至此，应用已具备首次交付给用户的条件。但应用并非发布一次就结束——你需要持续推出修复 Bug、增加功能的新版本。\u003c/p\u003e","title":"macOS 应用自主分发 (2): 创建 Sparkle 自动更新签名密钥"},{"content":"用 Developer ID 自主分发意味着什么 将 macOS 应用交付给用户，大致有两条路。一条是通过 Mac App Store (MAS)，另一条是由开发者自行制作 .dmg（或 .app）文件，让用户从网站或 GitHub 等地方下载的直接分发 (direct distribution)。\n直接分发有明显的优势：不必等待 App Store 审核，没有支付手续费，可以随时以自己想要的方式发布更新。但相应地，原本由 App Store 代劳的事情——代码签名、公证 (notarization)、自动更新——都需要开发者自行搭建。\n如果不搭建这些，macOS 的安全机制 Gatekeeper 会阻止应用启动。用户第一次打开应用时会看到类似\u0026quot;无法打开该 App，因为它来自身份不明的开发者\u0026quot;的警告，大多数用户就此放弃安装。要让应用像正规分发的应用那样双击即可打开，就必须用 Developer ID 证书签名应用，并获得 Apple 的公证。\n本系列介绍的正是这套一次性准备流程。好消息是，这里涉及的配置大多只需做一次，即可在所有后续发布中复用。\n本系列将完成的内容 共三篇，逐步搭建以下内容：\n(第 1 篇，本文) Developer ID Application 证书 + 公证用凭据 (第 2 篇) 用于 Sparkle 自动更新的 EdDSA 签名密钥 (第 3 篇) 托管更新 Feed 的公开仓库 + 收尾构建配置 系列结束时，你应该已经准备好以下内容。现在先浏览一下清单，每篇逐一完成即可。\nmacOS 钥匙串 (Keychain) 中的 Developer ID Application 证书 存储在钥匙串中的公证用凭据配置文件 用于 Sparkle 更新签名的 EdDSA 密钥对（公钥 + 私钥） 私钥备份到安全位置 用于托管更新 Feed (appcast) 的公开 GitHub 仓库 + GitHub Pages 构建配置文件 (ExportOptions.plist) 与应用侧配置 示例应用 — FocusTimer 本系列以虚构的 macOS 应用 FocusTimer（一款管理专注时间的简单计时器应用）为例。命令和路径中出现的 FocusTimer、Bundle Identifier com.example.FocusTimer、域名 example.com、证书名称、Team ID 等均为示例值。实际操作时请替换为你自己的应用名称和账户信息。\n本文基于 Xcode 26 和 Sparkle 2.x（自动更新框架，第 2 篇中会用到）编写。Apple 时常调整界面布局和菜单位置，若按钮名称略有不同，整体流程不变。\n前提条件 — 命令行工具 首先安装构建和发布自动化所需的两个命令行工具。前提是已安装 macOS 包管理器 Homebrew。\nbrew install gh create-dmg gh — GitHub 官方 CLI，稍后用于创建 GitHub Release。 create-dmg — 用于制作分发用 .dmg 磁盘镜像的工具，同时生成引导用户将应用拖入 Applications 文件夹的界面。 虽然现在还用不到，但发布自动化脚本通常会在开头检查这些工具是否存在，找不到就立即终止，因此提前安装好。\n第 1 步 — 确认 Apple Developer Program 要申请 Developer ID 证书并对应用进行公证，需要 Apple Developer Program 会员资格。个人账户每年收费 $99。\n如果已加入，只需确认状态是否为活跃即可。\n访问 developer.apple.com/account 在 Membership details 部分确认状态为 Active 在同一页面记下你的 Team ID（示例中为 ABCDE12345），后续步骤中会反复用到。 若尚未加入，会员资格审批通常需要约一天时间。审批完成前无法申请证书，建议优先处理。\n第 2 步 — 申请 Developer ID Application 证书 Developer ID Application 证书是对分发用 .app 和 .dmg 进行签名时使用的证书。macOS Gatekeeper 会将用此证书签名的应用识别为\u0026quot;已知开发者制作的应用\u0026quot;。\nApple 还有一种用于对 .pkg 安装包签名的 Developer ID Installer 证书，两者不同。本系列以 .dmg 方式分发，因此只涉及 Application 证书。\n申请流程 最简单的方式是通过 Xcode 操作。\n启动 Xcode → 菜单 Xcode → Settings… (⌘,) 选择 Accounts 标签 → 在左侧列表中点击你的 Apple ID 点击右下角的 Manage Certificates… 按钮 在新弹出的窗口左下角点击 + 按钮 从菜单中选择 Developer ID Application 一两秒后新证书出现在列表中，点击 Done 如此申请的证书会自动保存到 macOS 钥匙串 (Keychain) 中。证书与对应的私钥成对保存，有了这把私钥才能进行签名。\n确认申请结果 在终端运行以下命令，确认证书是否已正确安装。\nsecurity find-identity -v -p codesigning | grep \u0026#34;Developer ID Application\u0026#34; 如果输出如下一行，则表示成功。\n1) A1B2C3D4E5F6... \u0026#34;Developer ID Application: Gildong Hong (ABCDE12345)\u0026#34; 引号内的字符串是证书的正式名称。Gildong Hong 是注册在 Apple 账户中的姓名，括号内的 ABCDE12345 是第 1 步记下的 Team ID。这个名称在后续自动化代码签名时会原样使用，请再次核对。\n证书续期 Developer ID 证书的有效期为 5 年。临近过期时，Xcode 的 Manage Certificates 列表中会显示 Expired。届时按照相同步骤重新申请新证书即可。\n好消息是，已用旧证书签名并分发的应用不会在证书过期时立即失效。只有之后新建的构建才需要用新证书签名。因此无需过分担心过期问题。\n第 3 步 — 注册用于公证的 App 专用密码 什么是公证 (notarization) 公证是在分发前将应用上传至 Apple 服务器进行恶意软件扫描的流程。通过扫描后，Apple 会颁发\u0026quot;公证票据\u0026quot;，应用附带此票据后，Gatekeeper 才会无警告地开启应用。如果说签名证明\u0026quot;谁制作了它\u0026quot;，那么公证就是\u0026quot;Apple 已扫描过一次\u0026quot;的独立证明流程。\n公证提交使用 Apple 的 notarytool 命令，每次提交都会要求输入 Apple ID 密码。为避免每次手动输入，需要创建一个 App 专用密码 (App-Specific Password) 并提前存入钥匙串。\n3-1. 申请 App 专用密码 App 专用密码是一种 16 位密码，用于替代主 Apple ID 密码，仅供特定用途使用。\n访问 appleid.apple.com → 使用加入了 Apple Developer Program 的 Apple ID 登录（示例中为 developer@example.com） 登录与安全 (Sign-In and Security) → 选择 App 专用密码 (App-Specific Passwords) 点击 + 按钮 → 输入密码名称（如 focustimer-notarize） 点击 创建 (Create) → 再次输入 Apple ID 密码 屏幕上显示 abcd-efgh-ijkl-mnop 格式的 16 位密码。 关闭此页面后将无法再次查看该密码。 在进入下一步之前，请先将其复制到密码管理器等安全位置。\n3-2. 存入 notarytool 配置文件 将申请到的密码保存为钥匙串配置文件。执行以下命令前，请将 --password 的值替换为刚才获得的实际密码。\nxcrun notarytool store-credentials \u0026#34;focustimer-notarize\u0026#34; \\ --apple-id \u0026#34;developer@example.com\u0026#34; \\ --team-id \u0026#34;ABCDE12345\u0026#34; \\ --password \u0026#34;abcd-efgh-ijkl-mnop\u0026#34; 第一个参数 \u0026quot;focustimer-notarize\u0026quot; — 为此配置文件指定的名称，之后公证时将通过此名称加载凭据。 --apple-id — Apple Developer Program 账户的邮箱 --team-id — 第 1 步中的 Team ID --password — 3-1 中申请的 App 专用密码 出现如下输出即为成功。\nCredentials saved to Keychain. 凭据已存入钥匙串，App 专用密码的原始文本不再需要保留。（但若计划在其他电脑上也进行构建，建议保存在密码管理器中以备用。）\n3-3. 验证 确认已保存的配置文件是否实际可用。\nxcrun notarytool history --keychain-profile \u0026#34;focustimer-notarize\u0026#34; 若出现 Successfully received submission history. 的消息，则表示正常。其下的提交历史可能为空（因为尚未公证过任何内容，这是正常的），也可能有之前的记录。\n关键陷阱 — Apple ID 与 GitHub 账户是不同的 这是首次配置直接分发时最容易卡住的地方。\nApple Developer Program 账户 — 用于申请证书和公证（示例：developer@example.com） GitHub 账户 — 第 3 篇中用于托管更新文件（示例：myname@gmail.com） 两者通常使用不同的邮箱。特别注意：notarytool store-credentials 的 --apple-id 必须填写 Apple Developer 账户邮箱。若填写 GitHub 账户邮箱，认证会失败，而且错误信息不够友好，难以找到原因。\n若两个账户的邮箱容易混淆，建议记录下哪个是 Apple 用、哪个是 GitHub 用。这一区别在本系列中会反复出现。\n第 1 篇小结 跟到这里，你现在已经准备好以下内容：\n✅ 发布自动化用命令行工具（gh、create-dmg） ✅ 活跃状态的 Apple Developer Program 会员资格 ✅ 存储在钥匙串中的 Developer ID Application 证书 ✅ 存储在钥匙串中的公证用凭据配置文件（focustimer-notarize） 至此，签名和公证应用的准备工作已完成。但几乎没有应用只发布一次就结束。你需要不断推出修复 Bug、增加功能的新版本。如果是 App Store 应用，自动更新由 App Store 代劳；而直接分发则需要自行搭建这一机制。\n下一篇将为 macOS 应用事实上的标准自动更新框架 Sparkle 创建 EdDSA 签名密钥。这是独立于证书之外、专门用于验证更新文件的机制。\n","permalink":"https://hobbyworker.me/zh-hans/dev/2026-05-14-distribute-macos-app-1-developer-id-certificate/","summary":"\u003ch1 id=\"用-developer-id-自主分发意味着什么\"\u003e用 Developer ID 自主分发意味着什么\u003c/h1\u003e\n\u003cp\u003e将 macOS 应用交付给用户，大致有两条路。一条是通过 \u003cstrong\u003eMac App Store (MAS)\u003c/strong\u003e，另一条是由开发者自行制作 \u003ccode\u003e.dmg\u003c/code\u003e（或 \u003ccode\u003e.app\u003c/code\u003e）文件，让用户从网站或 GitHub 等地方下载的\u003cstrong\u003e直接分发 (direct distribution)\u003c/strong\u003e。\u003c/p\u003e","title":"macOS 应用自主分发 (1): Developer ID 证书与公证准备"},{"content":"概述 本报告中描述的过程将指导您如何在 macOS 中使用 Automator 创建一个快速操作。这个快速操作可用于将文本转换为更适合文件名的格式。转换过程中会删除特殊字符，将文本转换为小写，并将空格替换为连字符。\n步骤 打开 Automator 按 Cmd + Space 打开 Spotlight，输入 \u0026ldquo;Automator\u0026rdquo;，然后按 Enter。 创建一个新的服务 在 Automator 应用程序中，选择 \u0026ldquo;Quick Action\u0026rdquo;（以前称为 \u0026ldquo;Service\u0026rdquo;），然后点击 \u0026ldquo;Choose\u0026rdquo;（选择）。 配置快速操作 在新窗口的顶部，将 \u0026ldquo;Workflow receives current\u0026rdquo; 下拉菜单更改为 \u0026ldquo;text\u0026rdquo;（文本）。 确保 \u0026ldquo;in\u0026rdquo; 下拉菜单设置为 \u0026ldquo;any application\u0026rdquo;（任何应用程序）。 添加 \u0026ldquo;Run Shell Script\u0026rdquo; 操作 在左侧的搜索栏中输入 \u0026ldquo;Run Shell Script\u0026rdquo;，并将操作拖到右侧的面板。 配置 \u0026ldquo;Run Shell Script\u0026rdquo; 操作 将 \u0026ldquo;Pass input\u0026rdquo; 更改为 \u0026ldquo;as arguments\u0026rdquo;（作为参数）。 在文本框中粘贴以下脚本： for text_input in \u0026#34;$@\u0026#34; do echo \u0026#34;$text_input\u0026#34; | sed \u0026#39;s/[^a-zA-Z0-9 ]//g\u0026#39; | tr \u0026#39;[:upper:]\u0026#39; \u0026#39;[:lower:]\u0026#39; | sed \u0026#39;s/ /-/g\u0026#39; done 添加 \u0026ldquo;Copy to Clipboard\u0026rdquo; 操作 在左侧的搜索栏中输入 \u0026ldquo;Copy to Clipboard\u0026rdquo;，并将操作拖到右侧面板的 \u0026ldquo;Run Shell Script\u0026rdquo; 操作下方。 保存快速操作 按 Cmd + S，为你的快速操作命名，例如 \u0026ldquo;将文本转换为文件名\u0026rdquo;。 现在，脚本应已准备好从 macOS 服务支持的任何文本编辑器的右键上下文菜单中使用。\n要使用脚本：\n在文本编辑器中选择一个文本块。 在选定的文本上右键单击。 转到 \u0026ldquo;Services\u0026rdquo;（服务）或 \u0026ldquo;Quick Actions\u0026rdquo;（快速操作）（取决于你的 macOS 版本）。 选择 \u0026ldquo;将文本转换为文件名\u0026rdquo; 操作。 处理后的文本将复制到剪贴板，您可以将其粘贴到所需的任何地方。\n结论 使用 Automator 在 macOS 中创建快速操作是一种便捷的方式，可以通过自动化重复任务（如将文本转换为适合文件名的格式）来简化工作流程。本报告提供了有关如何创建可在各种 macOS 应用程序中使用的快速操作的详细步骤。这将成为那些希望提高生产力和简化文本编辑过程的人们的重要工具。\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-04-06-convert-text-to-filename-using-automator-on-macos/","summary":"\u003ch1 id=\"概述\"\u003e概述\u003c/h1\u003e\n\u003cp\u003e本报告中描述的过程将指导您如何在 macOS 中使用 Automator 创建一个快速操作。这个快速操作可用于将文本转换为更适合文件名的格式。转换过程中会删除特殊字符，将文本转换为小写，并将空格替换为连字符。\u003c/p\u003e","title":"在 macOS 上使用 Automator 将文本转换为文件名"},{"content":"概述 pytrends 库中的 realtime_trending_searches() 函数允许您检索Google上的实时热门搜索。通过分析这些数据，您可以洞察吸引受众注意力的最新趋势和主题，帮助您创建及时、相关且吸引人的内容。\n在本教程中，我们将介绍：\n导入必要的库 设置 pytrends 请求 检索实时热门搜索数据 分析结果 检索实时热门搜索数据 首先，我们需要导入必要的库并设置 pytrends 请求。\nfrom pytrends.request import TrendReq # Set up pytrends request pytrends = TrendReq(hl=\u0026#39;en-US\u0026#39;, tz=360) 接下来，我们将使用 realtime_trending_searches() 函数检索Google上当前的实时热门搜索。\n# Retrieve real-time trending searches data realtime_trending_searches = pytrends.realtime_trending_searches(pn=\u0026#39;US\u0026#39;) 这将返回一个DataFrame，其中包含美国当前的实时热门搜索。\n分析结果 现在，我们可以分析实时热门搜索数据，以识别吸引受众注意力的最新趋势和主题。\n# Display the top 10 real-time trending searches print(realtime_trending_searches.head(10)) 这将显示前10个实时热门搜索，为您提供有关吸引受众注意力的最新趋势和主题的宝贵洞察。\n结论 在本文中，我们演示了如何使用 pytrends 库中的 realtime_trending_searches() 函数发现Google上的实时热门搜索。通过分析这些数据，您可以掌握最新的趋势和主题，帮助您创建及时、相关且吸引人的内容。本教程涵盖了从设置 pytrends 请求到分析结果的整个收集和分析实时热门搜索数据的过程。利用这些洞察，您可以创建与受众产生共鸣并利用当前趋势的内容。\nNOTE : pytrends uses an unofficial API. Please use here for issues.\nSAMPLE CODE : https://github.com/hobbyworker/google-trend-for-python\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-04-05-pytrends-11-discovering-realtime-trending-searches-for-uptotheminute-insights/","summary":"\u003ch1 id=\"概述\"\u003e概述\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003epytrends\u003c/code\u003e 库中的 \u003ccode\u003erealtime_trending_searches()\u003c/code\u003e 函数允许您检索Google上的实时热门搜索。通过分析这些数据，您可以洞察吸引受众注意力的最新趋势和主题，帮助您创建及时、相关且吸引人的内容。\u003c/p\u003e","title":"Pytrends 11: 发现实时热门搜索以获得最新洞察"},{"content":"概述 pytrends 库中的 suggestions() 函数允许您检索特定查询的搜索建议。通过分析这些建议，您可以发现与搜索查询相关的新关键词和趋势，帮助您创建更具吸引力和针对性的内容。\n在本教程中，我们将介绍：\n导入必要的库 设置 pytrends 请求 检索搜索建议数据 分析结果 检索搜索建议数据 首先，我们需要导入必要的库并设置 pytrends 请求。\nfrom pytrends.request import TrendReq # Set up pytrends request pytrends = TrendReq(hl=\u0026#39;en-US\u0026#39;, tz=360) 接下来，我们将使用 suggestions() 函数检索特定查询的搜索建议。\n# Retrieve search suggestions for the query \u0026#39;Python\u0026#39; suggestions = pytrends.suggestions(keyword=\u0026#39;Python\u0026#39;) 这将返回一个字典列表，其中包含与查询\u0026rsquo;Python\u0026rsquo;相关的搜索建议。\n分析结果 现在，我们可以分析搜索建议数据，以发现与搜索查询相关的新关键词和趋势。\n# Display the search suggestions for suggestion in suggestions: print(suggestion[\u0026#39;title\u0026#39;]) 这将显示与查询\u0026rsquo;Python\u0026rsquo;相关的搜索建议，为您提供有关与搜索查询相关的新关键词和趋势的宝贵洞察。\n结论 在本文中，我们演示了如何使用 pytrends 库中的 suggestions() 函数通过获取基于给定查询的搜索建议来优化您的趋势搜索。通过探索这些建议，您可以发现与搜索查询相关的新关键词和趋势，帮助您创建更具吸引力和针对性的内容。本教程涵盖了从设置 pytrends 请求到分析结果的整个收集和分析搜索建议数据的过程。利用这些洞察，您可以增强您的关键词研究和内容策略。\nNOTE : pytrends uses an unofficial API. Please use here for issues.\nSAMPLE CODE : https://github.com/hobbyworker/google-trend-for-python\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-04-04-pytrends-10-refining-trend-searches-with-suggestions/","summary":"\u003ch1 id=\"概述\"\u003e概述\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003epytrends\u003c/code\u003e 库中的 \u003ccode\u003esuggestions()\u003c/code\u003e 函数允许您检索特定查询的搜索建议。通过分析这些建议，您可以发现与搜索查询相关的新关键词和趋势，帮助您创建更具吸引力和针对性的内容。\u003c/p\u003e","title":"Pytrends 10: 通过搜索建议优化趋势搜索"},{"content":"概述 pytrends 库中的 top_charts() 函数允许您检索Google针对特定年份和类别的热门排行榜。通过分析这些数据，您可以发现各种类别中最热门的搜索查询，帮助您为受众创建有吸引力且相关的内容。\n在本教程中，我们将介绍：\n导入必要的库 设置 pytrends 请求 检索热门排行榜数据 分析结果 检索热门排行榜数据 首先，我们需要导入必要的库并设置 pytrends 请求。\nfrom pytrends.request import TrendReq # Set up pytrends request pytrends = TrendReq(hl=\u0026#39;en-US\u0026#39;, tz=360) 接下来，我们将使用 top_charts() 函数检索特定年份和类别的热门排行榜数据。\n# Retrieve top charts data for 2022 top_charts = pytrends.top_charts(date=2022, hl=\u0026#39;en-US\u0026#39;, tz=360) 这将返回一个DataFrame，其中包含2022年的热门排行榜数据。\n分析结果 现在，我们可以分析热门排行榜数据，以识别我们所选类别中最热门的搜索查询。\n# Display the top 10 in 2022 print(top_charts.head(10)) 这将显示2022年的前10名，为您提供有关最热门搜索查询的宝贵洞察。\n结论 在本文中，我们演示了如何使用 pytrends 库中的 top_charts() 函数分析Google的热门排行榜，以获得数据驱动的洞察。通过探索这些数据，您可以发现各种类别中最热门的搜索查询，帮助您为受众创建有吸引力且相关的内容。本教程涵盖了从设置 pytrends 请求到分析结果的整个收集和分析热门排行榜数据的过程。利用这些洞察，您可以为内容策略提供参考并优化您的在线影响力。\nNOTE : pytrends uses an unofficial API. Please use here for issues.\nSAMPLE CODE : https://github.com/hobbyworker/google-trend-for-python\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-04-03-pytrends-9-mastering-top-charts-analysis-for-datadriven-insights/","summary":"\u003ch1 id=\"概述\"\u003e概述\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003epytrends\u003c/code\u003e 库中的 \u003ccode\u003etop_charts()\u003c/code\u003e 函数允许您检索Google针对特定年份和类别的热门排行榜。通过分析这些数据，您可以发现各种类别中最热门的搜索查询，帮助您为受众创建有吸引力且相关的内容。\u003c/p\u003e","title":"Pytrends 9: 掌握热门排行榜分析以获得数据驱动的洞察"},{"content":"概述 pytrends 库中的 trending_searches() 函数允许您检索Google上当前的热门搜索。通过分析这些数据，您可以洞察吸引受众注意力的最新趋势和主题，帮助您创建及时且相关的内容。\n在本教程中，我们将介绍：\n导入必要的库 设置 pytrends 请求 检索热门搜索数据 分析结果 检索热门搜索数据 首先，我们需要导入必要的库并设置 pytrends 请求。\nfrom pytrends.request import TrendReq # Set up pytrends request pytrends = TrendReq(hl=\u0026#39;en-US\u0026#39;, tz=360) 接下来，我们将使用 trending_searches() 函数检索Google上当前的热门搜索。\n# Retrieve trending searches data trending_searches = pytrends.trending_searches(pn=\u0026#39;united_states\u0026#39;) 这将返回一个DataFrame，其中包含美国当前的热门搜索。\n分析结果 现在，我们可以分析热门搜索数据，以识别内容创作和优化的新机会。\n# Display the top 10 trending searches print(trending_searches.head(10)) 这将显示前10个热门搜索，为您提供有关吸引受众注意力的最新趋势和主题的宝贵洞察。\n结论 在本文中，我们演示了如何使用 pytrends 库中的 trending_searches() 函数跟踪Google上的热门搜索。通过分析这些数据，您可以保持领先并发现内容创作和优化的新机会。本教程涵盖了从设置 pytrends 请求到分析结果的整个收集和分析热门搜索数据的过程。利用这些洞察，您可以为受众创建及时且相关的内容。\nNOTE : pytrends uses an unofficial API. Please use here for issues.\nSAMPLE CODE : https://github.com/hobbyworker/google-trend-for-python\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-04-02-pytrends-8-tracking-trending-searches-to-stay-ahead/","summary":"\u003ch1 id=\"概述\"\u003e概述\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003epytrends\u003c/code\u003e 库中的 \u003ccode\u003etrending_searches()\u003c/code\u003e 函数允许您检索Google上当前的热门搜索。通过分析这些数据，您可以洞察吸引受众注意力的最新趋势和主题，帮助您创建及时且相关的内容。\u003c/p\u003e","title":"Pytrends 8: 跟踪热门搜索以保持领先"},{"content":"概述 pytrends 库中的 related_queries() 函数允许您检索特定搜索词的相关查询。通过分析这些数据，您可以洞察对您的受众重要的问题和主题，帮助您创建更相关和吸引人的内容。\n在本教程中，我们将介绍：\n导入必要的库 设置 pytrends 请求 检索相关查询数据 分析结果 检索相关查询数据 首先，我们需要导入必要的库并设置 pytrends 请求。\nfrom pytrends.request import TrendReq # Set up pytrends request pytrends = TrendReq(hl=\u0026#39;en-US\u0026#39;, tz=360) 接下来，我们将使用 build_payload() 函数指定搜索词，然后使用 related_queries() 函数检索相关查询数据。\nkeywords = [\u0026#39;Python\u0026#39;] # Build payload pytrends.build_payload(keywords, timeframe=\u0026#39;now 7-d\u0026#39;, geo=\u0026#39;\u0026#39;) # Retrieve related queries data related_queries = pytrends.related_queries() 这将返回一个字典，其中包含搜索词\u0026rsquo;Python\u0026rsquo;在过去7天内的相关查询数据。\n分析结果 现在，我们可以分析相关查询数据，以识别内容创作和优化的新机会。\n# Extract the related queries for the keyword \u0026#39;Python\u0026#39; python_related_queries = related_queries[keywords[0]][\u0026#39;rising\u0026#39;] # Display the top 10 rising related queries print(python_related_queries.head(10)) 这将显示搜索词\u0026rsquo;Python\u0026rsquo;的前10个上升相关查询，为您提供有关受众重要问题和主题的宝贵洞察。\n结论 在本文中，我们演示了如何使用 pytrends 库中的 related_queries() 函数发现给定搜索词的相关查询。通过分析这些数据，您可以对目标关键词进行深入分析，并发现内容创作和优化的新机会。本教程涵盖了从设置 pytrends 请求到分析结果的整个收集和分析相关查询数据的过程。利用这些洞察，您可以为受众创建更相关和吸引人的内容。\nNOTE : pytrends uses an unofficial API. Please use here for issues.\nSAMPLE CODE : https://github.com/hobbyworker/google-trend-for-python\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-04-01-pytrends-7-uncovering-related-queries-for-indepth-analysis/","summary":"\u003ch1 id=\"概述\"\u003e概述\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003epytrends\u003c/code\u003e 库中的 \u003ccode\u003erelated_queries()\u003c/code\u003e 函数允许您检索特定搜索词的相关查询。通过分析这些数据，您可以洞察对您的受众重要的问题和主题，帮助您创建更相关和吸引人的内容。\u003c/p\u003e\n\u003cp\u003e在本教程中，我们将介绍：\u003c/p\u003e","title":"Pytrends 7: 发现相关查询以进行深入分析"},{"content":"概述 pytrends 库中的 related_topics() 函数允许您检索特定搜索词的相关主题。通过分析这些数据，您可以发现与目标受众相关的新关键词和创意，帮助您创建有吸引力且相关的内容。\n在本教程中，我们将介绍：\n导入必要的库 设置 pytrends 请求 检索相关主题数据 分析结果 检索相关主题数据 首先，我们需要导入必要的库并设置 pytrends 请求。\nfrom pytrends.request import TrendReq # Set up pytrends request pytrends = TrendReq(hl=\u0026#39;en-US\u0026#39;, tz=360) 接下来，我们将使用 build_payload() 函数指定搜索词，然后使用 related_topics() 函数检索相关主题数据。\nkeywords = [\u0026#39;Python\u0026#39;] # Build payload pytrends.build_payload(keywords, timeframe=\u0026#39;now 7-d\u0026#39;, geo=\u0026#39;\u0026#39;) # Retrieve related topics data related_topics = pytrends.related_topics() 这将返回一个字典，其中包含搜索词\u0026rsquo;Python\u0026rsquo;在过去7天内的相关主题数据。\n分析结果 现在，我们可以分析相关主题数据，以识别用于内容策略的新关键词和创意。\n# Extract the related topics for the keyword \u0026#39;Python\u0026#39; python_related_topics = related_topics[keywords[0]][\u0026#39;top\u0026#39;] # Display the top 10 rising related topics print(python_related_topics.head(10)) 这将显示搜索词\u0026rsquo;Python\u0026rsquo;的前10个上升相关主题，为您提供有关与您的关键词相关的新兴趋势的宝贵洞察。\n结论 在本文中，我们演示了如何使用 pytrends 库中的 related_topics() 函数调查给定搜索词的相关主题。通过探索这些数据，您可以扩展您的关键词研究并发现吸引目标受众的新机会。本教程涵盖了从设置 pytrends 请求到分析结果的整个收集和分析相关主题数据的过程。利用这些洞察，您可以为内容策略提供参考并提升您的在线影响力。\nNOTE : pytrends uses an unofficial API. Please use here for issues.\nSAMPLE CODE : https://github.com/hobbyworker/google-trend-for-python\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-31-pytrends-6-investigating-related-topics-to-expand-keyword-research/","summary":"\u003ch1 id=\"概述\"\u003e概述\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003epytrends\u003c/code\u003e 库中的 \u003ccode\u003erelated_topics()\u003c/code\u003e 函数允许您检索特定搜索词的相关主题。通过分析这些数据，您可以发现与目标受众相关的新关键词和创意，帮助您创建有吸引力且相关的内容。\u003c/p\u003e\n\u003cp\u003e在本教程中，我们将介绍：\u003c/p\u003e","title":"Pytrends 6: 调查相关主题以扩展关键词研究"},{"content":"概述 pytrends 库中的 interest_by_region() 函数允许您检索特定搜索词在不同地理位置的兴趣数据。通过分析这些数据，您可以获得不同地区搜索词受欢迎程度的宝贵洞察，从而为营销和内容策略提供参考。\n在本教程中，我们将介绍：\n导入必要的库 设置 pytrends 请求 检索按地区划分的兴趣数据 可视化结果 安装 要安装Pytrends，只需使用pip：\npip install matplotlib 检索按地区划分的兴趣数据 首先，我们需要导入必要的库并设置 pytrends 请求。\nfrom pytrends.request import TrendReq import pandas as pd # Set up pytrends request pytrends = TrendReq(hl=\u0026#39;en-US\u0026#39;, tz=360) 接下来，我们将使用 build_payload() 函数指定搜索词和时间范围，然后使用 interest_by_region() 函数检索按地区划分的兴趣数据。\nkeywords = [\u0026#39;Python\u0026#39;] timeframe = \u0026#39;2023-01-01 2023-03-31\u0026#39; # Build payload pytrends.build_payload(keywords, timeframe=timeframe, geo=\u0026#39;\u0026#39;) # Retrieve interest by region data region_interest = pytrends.interest_by_region(resolution=\u0026#39;COUNTRY\u0026#39;, inc_low_vol=True, inc_geo_code=False) 这将返回一个DataFrame，其中包含搜索词\u0026rsquo;Python\u0026rsquo;在2023年第一季度按地区划分的兴趣数据。\n可视化结果 现在，我们可以使用条形图来可视化按地区划分的兴趣数据。\nimport matplotlib.pyplot as plt # Sort the data by interest value region_interest = region_interest.sort_values(by=\u0026#39;Python\u0026#39;, ascending=False) # Plot the interest by region data plt.figure(figsize=(12, 6)) plt.bar(region_interest.index, region_interest[\u0026#39;Python\u0026#39;]) plt.xlabel(\u0026#39;Country\u0026#39;) plt.ylabel(\u0026#39;Interest\u0026#39;) plt.title(\u0026#39;Interest by Region for Python (Q1 2023)\u0026#39;) plt.xticks(rotation=90) plt.show() 此图显示了搜索词\u0026rsquo;Python\u0026rsquo;在不同国家/地区的兴趣，让您能够识别该词特别受欢迎的地区。\n结论 在本文中，我们演示了如何使用 pytrends 库中的 interest_by_region() 函数分析特定搜索词按地区划分的兴趣。通过探索这些数据，您可以获得不同地理位置搜索词受欢迎程度的有针对性洞察，帮助您更好地理解受众并优化营销策略。本教程涵盖了从设置 pytrends 请求到可视化结果的整个收集和分析按地区划分的兴趣数据的过程。\nNOTE : pytrends uses an unofficial API. Please use here for issues.\nSAMPLE CODE : https://github.com/hobbyworker/google-trend-for-python\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-30-pytrends-5-exploring-interest-by-region-for-targeted-insights/","summary":"\u003ch1 id=\"概述\"\u003e概述\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003epytrends\u003c/code\u003e 库中的 \u003ccode\u003einterest_by_region()\u003c/code\u003e 函数允许您检索特定搜索词在不同地理位置的兴趣数据。通过分析这些数据，您可以获得不同地区搜索词受欢迎程度的宝贵洞察，从而为营销和内容策略提供参考。\u003c/p\u003e","title":"Pytrends 5: 探索按地区划分的兴趣以获得有针对性的洞察"},{"content":"概述 pytrends 库中的 get_historical_interest() 函数允许您检索特定搜索词在给定时间范围内的小时兴趣数据。这有助于更细致地了解搜索词的受欢迎程度，并识别在查看每日或每周数据时可能不可见的趋势。\n在本教程中，我们将介绍：\n导入必要的库 设置 pytrends 请求 检索历史小时兴趣数据 可视化结果 安装 要安装Pytrends，只需使用pip：\npip install matplotlib 检索历史小时兴趣数据 首先，我们需要导入必要的库并设置 pytrends 请求。\nfrom pytrends.request import TrendReq import pandas as pd import matplotlib.pyplot as plt # Set up pytrends request pytrends = TrendReq(hl=\u0026#39;en-US\u0026#39;, tz=360) 接下来，我们将使用 get_historical_interest() 函数指定搜索词、时间范围和其他请求参数。\nkeywords = [\u0026#39;Python\u0026#39;, \u0026#39;JavaScript\u0026#39;] # Retrieve hourly interest data hourly_interest = pytrends.get_historical_interest(keywords, year_start=2023, month_start=3, day_start=1, hour_start=0, year_end=2023, month_end=3, day_end=2, hour_end=0, cat=0, geo=\u0026#39;\u0026#39;, gprop=\u0026#39;\u0026#39;, sleep=0) 这将返回一个DataFrame，其中包含搜索词\u0026rsquo;Python\u0026rsquo;和\u0026rsquo;JavaScript\u0026rsquo;从2023年3月1日到3月2日的小时兴趣数据。\n可视化结果 现在，我们可以使用简单的折线图来可视化小时兴趣数据。\n# Plot the hourly interest data plt.figure(figsize=(12, 6)) plt.plot(hourly_interest.index, hourly_interest[\u0026#39;Python\u0026#39;], label=\u0026#39;Python\u0026#39;) plt.plot(hourly_interest.index, hourly_interest[\u0026#39;JavaScript\u0026#39;], label=\u0026#39;JavaScript\u0026#39;) plt.xlabel(\u0026#39;Hour\u0026#39;) plt.ylabel(\u0026#39;Interest\u0026#39;) plt.title(\u0026#39;Hourly Interest for Python and JavaScript\u0026#39;) plt.legend() plt.show() 此图显示了\u0026rsquo;Python\u0026rsquo;和\u0026rsquo;JavaScript\u0026rsquo;在指定时间范围内的小时兴趣，使您可以比较它们的受欢迎程度并识别趋势。\n结论 在本文中，我们演示了如何使用 pytrends 库中的 get_historical_interest() 函数从Google Trends检索历史小时兴趣数据。通过深入研究这些数据，您可以获得有关搜索词受欢迎程度的宝贵洞察，并更好地理解消费者行为。本教程涵盖了从设置 pytrends 请求到可视化结果的整个收集和分析小时兴趣数据的过程。\nNOTE : pytrends uses an unofficial API. Please use here for issues.\nSAMPLE CODE : https://github.com/hobbyworker/google-trend-for-python\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-29-pytrends-4-diving-into-historical-hourly-interest-data/","summary":"\u003ch1 id=\"概述\"\u003e概述\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003epytrends\u003c/code\u003e 库中的 \u003ccode\u003eget_historical_interest()\u003c/code\u003e 函数允许您检索特定搜索词在给定时间范围内的小时兴趣数据。这有助于更细致地了解搜索词的受欢迎程度，并识别在查看每日或每周数据时可能不可见的趋势。\u003c/p\u003e","title":"Pytrends 4: 深入研究历史小时兴趣数据"},{"content":"概述 pytrends 库中的 multirange_interest_over_time() 函数允许您检索特定关键词在多个时间范围内的兴趣。通过分析这些数据，您可以洞察关键词在不同时期内兴趣的演变情况，从而为内容策略和营销工作做出明智的决策。\n在本教程中，我们将介绍：\n导入必要的库 设置 pytrends 请求 构建时间范围列表 检索多时间范围的随时间变化兴趣数据 分析结果 构建时间范围列表 首先，我们需要创建一个时间范围列表，用于分析我们关键词的兴趣。在此示例中，我们将创建一个包含两个时间范围的列表\ntime_ranges = [ \u0026#39;2022-09-04 2022-09-10\u0026#39;, \u0026#39;2022-09-18 2022-09-24\u0026#39;, ] 检索多时间范围的随时间变化兴趣数据 接下来，我们需要导入必要的库，设置 pytrends 请求，并检索关键词和时间范围的多时间范围随时间变化兴趣数据。\nfrom pytrends.request import TrendReq # Set up pytrends request pytrends = TrendReq(hl=\u0026#39;en-US\u0026#39;, tz=360) # Define the list of keywords keywords = [\u0026#39;pizza\u0026#39;, \u0026#39;bagel\u0026#39;] # Build the payload pytrends.build_payload(keywords, timeframe=time_ranges) # Retrieve multi-range interest over time data interest_over_time_data = pytrends.multirange_interest_over_time() 这将返回一个字典，其中包含我们关键词在指定时间范围内的随时间变化兴趣数据。\n分析结果 现在，我们可以分析多时间范围的随时间变化兴趣数据，以了解关键词在不同时期的表现和受欢迎程度。\n# Display the interest over time data print(interest_over_time_data) 这将显示每个时间范围的随时间变化兴趣数据，为我们关键词在不同时期的表现和受欢迎程度提供宝贵的洞察。\n结论 在本文中，我们演示了如何使用 pytrends 库中的 multirange_interest_over_time() 函数分析特定关键词在多个时间范围内的兴趣。通过分析这些数据，您可以获得更全面的趋势视角，并了解关键词在不同时期内兴趣的演变情况。这些信息可以帮助您就内容策略、营销工作甚至产品开发做出明智的决策。通过利用多时间范围的随时间变化兴趣分析，您可以保持竞争优势，并确保您的内容和产品始终与目标受众相关且具有吸引力。\nNOTE : pytrends uses an unofficial API. Please use here for issues.\nSAMPLE CODE : https://github.com/hobbyworker/google-trend-for-python\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-28-pytrends-3-harnessing-multirange-interest-over-time-analysis/","summary":"\u003ch1 id=\"概述\"\u003e概述\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003epytrends\u003c/code\u003e 库中的 \u003ccode\u003emultirange_interest_over_time()\u003c/code\u003e 函数允许您检索特定关键词在多个时间范围内的兴趣。通过分析这些数据，您可以洞察关键词在不同时期内兴趣的演变情况，从而为内容策略和营销工作做出明智的决策。\u003c/p\u003e","title":"Pytrends 3: 利用多时间范围的随时间变化兴趣分析"},{"content":"概述 pytrends 库中的 interest_over_time() 函数允许您检索特定关键词随时间变化的兴趣。通过分析这些数据，您可以洞察关键词兴趣的演变情况，从而为内容策略和营销工作做出明智的决策。\n在本教程中，我们将介绍：\n定义关键词列表 设置时间范围 检索随时间变化的兴趣数据 分析结果 定义关键词列表 首先，我们需要定义要分析其随时间变化兴趣的关键词列表。\nkeywords = [\u0026#39;Python\u0026#39;, \u0026#39;JavaScript\u0026#39;] 设置时间范围 接下来，我们需要设置要分析关键词兴趣的时间范围。在此示例中，我们将分析过去一年的兴趣。\ntime_range = \u0026#39;2022-01-01 2023-01-31\u0026#39; 检索随时间变化的兴趣数据 现在，我们需要使用 interest_over_time() 函数检索关键词和时间范围的随时间变化的兴趣数据。\nfrom pytrends.request import TrendReq # Set up pytrends request pytrends = TrendReq(hl=\u0026#39;en-US\u0026#39;, tz=360) # Build the payload pytrends.build_payload(keywords, cat=0, timeframe=time_range, geo=\u0026#39;\u0026#39;, gprop=\u0026#39;\u0026#39;) # Retrieve interest over time data interest_over_time_data = pytrends.interest_over_time() 这将返回一个包含关键词随时间变化兴趣数据的DataFrame。\n分析结果 现在，我们可以分析随时间变化的兴趣数据，以了解关键词的表现和受欢迎程度。\nprint(interest_over_time_data.head()) 这将显示随时间变化的兴趣数据，为我们关键词的表现和受欢迎程度提供宝贵的洞察。\n结论 在本文中，我们演示了如何使用 pytrends 库中的 interest_over_time() 函数分析特定关键词随时间变化的兴趣。通过分析这些数据，您可以洞察关键词兴趣的演变情况，帮助您就内容策略、营销工作甚至产品开发做出明智的决策。通过利用随时间变化的兴趣分析，您可以保持竞争优势，并确保您的内容和产品始终与目标受众相关且具有吸引力。\nNOTE : pytrends uses an unofficial API. Please use here for issues.\nSAMPLE CODE : https://github.com/hobbyworker/google-trend-for-python\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-27-pytrends-2-analyzing-interest-over-time/","summary":"\u003ch1 id=\"概述\"\u003e概述\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003epytrends\u003c/code\u003e 库中的 \u003ccode\u003einterest_over_time()\u003c/code\u003e 函数允许您检索特定关键词随时间变化的兴趣。通过分析这些数据，您可以洞察关键词兴趣的演变情况，从而为内容策略和营销工作做出明智的决策。\u003c/p\u003e","title":"Pytrends 2: 分析随时间变化的兴趣"},{"content":"概述 Google Trends提供了关于搜索趋势和关键词流行度的宝贵洞察。然而，Google并未提供访问此数据的官方API。幸运的是，Pytrends库使我们能够使用Python访问Google Trends数据。\n在本教程中，我们将引导您完成Pytrends的安装和设置，并演示如何执行简单的搜索和解释结果。\n前提条件 要按照本教程操作，您应该具备：\n已安装Python 3 具有Python编程的基础知识 熟悉Python包的使用 安装 要安装Pytrends，只需使用pip：\npip install pytrends 设置Pytrends 要开始使用Pytrends，首先导入必要的库并建立与Google Trends的连接：\nfrom pytrends.request import TrendReq pytrends = TrendReq(hl=\u0026#39;en-US\u0026#39;, tz=360) 在这里，我们将语言设置为英语（hl='en-US'），时区设置为UTC+0（tz=360）。\n执行基本搜索 现在，让我们执行一个简单的搜索，查看关键词\u0026quot;Python\u0026quot;随时间的兴趣变化：\nkeywords = [\u0026#39;Python\u0026#39;] pytrends.build_payload(keywords, timeframe=\u0026#39;today 5-y\u0026#39;, geo=\u0026#39;\u0026#39;, gprop=\u0026#39;\u0026#39;) interest_over_time_df = pytrends.interest_over_time() print(interest_over_time_df) 这段代码定义了一个关键词列表，将时间范围设置为过去五年（timeframe='today 5-y'），并将地理位置和Google属性留空。interest_over_time() 方法返回一个包含兴趣数据的DataFrame。\n理解结果 返回的DataFrame包含关键词\u0026quot;Python\u0026quot;在过去五年中的搜索兴趣。这些值表示相对于指定时间范围内最高点的搜索兴趣，100代表流行度的峰值。\n结论 在本文中，我们介绍了Pytrends——一个非官方的Python版Google Trends API，并演示了如何安装和设置它。我们使用该库执行了基本搜索，并讨论了如何解释结果。\n在接下来的文章中，我们将深入研究更高级的Pytrends功能，例如分析随时间变化的兴趣、按地区探索兴趣，以及发现相关主题和查询。敬请期待！\nNOTE : pytrends uses an unofficial API. Please use here for issues.\nSAMPLE CODE : https://github.com/hobbyworker/google-trend-for-python\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-26-pytrends-1-how-to-use-google-trend-unofficially-with-python/","summary":"\u003ch1 id=\"概述\"\u003e概述\u003c/h1\u003e\n\u003cp\u003eGoogle Trends提供了关于搜索趋势和关键词流行度的宝贵洞察。然而，Google并未提供访问此数据的官方API。幸运的是，Pytrends库使我们能够使用Python访问Google Trends数据。\u003c/p\u003e","title":"Pytrends 1: 如何使用Python非官方地使用Google趋势"},{"content":"概述 rustup 是 Rust 编程语言的官方工具链安装程序和管理器。它提供了一种方便的方式来在您的系统上安装、更新和管理多个 Rust 工具链。本报告将涵盖各种平台的安装过程、rustup 的基本用法，并提供一个管理多个 Rust 环境的示例。\n安装 macOS 和 Linux 要在 macOS 和 Linux 系统上安装 rustup，打开您的终端并输入以下命令：\ncurl --proto \u0026#39;=https\u0026#39; --tlsv1.2 -sSf https://sh.rustup.rs | sh 脚本将下载并安装必要的组件。安装完成后，请重新启动您的终端或运行以下命令更新您的 shell 环境变量：\nsource $HOME/.cargo/env Windows 对于 Windows 用户，请从官方 Rust 网站下载并运行 rustup-init.exe 可执行文件。按照屏幕上的说明完成安装。安装完成后，请重新启动命令提示符或终端。\n用法 安装特定版本的 Rust 要安装特定版本的 Rust，请使用以下命令：\nrustup install \u0026lt;version\u0026gt; 将 \u0026lt;version\u0026gt; 替换为所需的 Rust 版本，例如 1.52.0。\n设置默认的 Rust 版本 要为新项目设置默认的 Rust 版本，请使用以下命令：\nrustup default \u0026lt;version\u0026gt; 将 \u0026lt;version\u0026gt; 替换为所需的 Rust 版本，例如 1.52.0。\n在不同的 Rust 版本之间切换 要在特定项目之间切换不同的 Rust 版本，请导航到该项目目录并使用以下命令：\nrustup override set \u0026lt;version\u0026gt; 将 \u0026lt;version\u0026gt; 替换为所需的 Rust 版本，例如 1.52.0。\n更新 Rust 要将所有已安装的 Rust 工具链更新到其最新版本，请运行以下命令：\nrustup update 卸载 Rust 要从您的系统中卸载 Rust 和 rustup，请运行以下命令：\nrustup self uninstall 示例 假设您正在开发两个 Rust 项目：project_old 和 project_new。project_old 需要 Rust 版本 1.52.0，而 project_new 需要最新的稳定版本。\n首先，安装所需的 Rust 版本：\nrustup install 1.52.0 rustup install stable 接下来，导航到 project_old 目录并设置项目的 Rust 版本：\ncd project_old rustup override set 1.52.0 现在，导航到 project_new 目录并设置项目的 Rust 版本：\ncd project_new rustup override set stable 有了这些配置，每个项目在构建或运行时将使用适当的 Rust 版本。\n例如，当您在 project_old 目录中运行 cargo build 或 cargo run 时，将使用 Rust 1.52.0：\ncd project_old cargo build 同样地，当您在 project_new 目录中运行 cargo build 或 cargo run 时，将使用最新的稳定 Rust 版本：\ncd project_new cargo build 使用 rustup，您可以无缝地处理具有不同 Rust 版本要求的多个项目，而无需进行任何冲突或手动干预。\n结论 rustup 是 Rust 开发者的必备工具，因为它简化了在单个系统上管理多个 Rust 环境的过程。本报告涵盖了各种平台的安装过程、基本用法，并提供了一个管理多个项目的不同 Rust 版本的示例。通过使用 rustup，开发者可以确保其项目始终使用正确的 Rust 版本构建和运行，提高生产力并减少版本相关的问题的可能性。\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-26-managing-multiple-rust-environments-with-rustup/","summary":"\u003ch1 id=\"概述\"\u003e概述\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003erustup\u003c/code\u003e 是 Rust 编程语言的官方工具链安装程序和管理器。它提供了一种方便的方式来在您的系统上安装、更新和管理多个 Rust 工具链。本报告将涵盖各种平台的安装过程、\u003ccode\u003erustup\u003c/code\u003e 的基本用法，并提供一个管理多个 Rust 环境的示例。\u003c/p\u003e","title":"使用 rustup 管理多个 Rust 环境"},{"content":"在本博客文章中，我们将讨论如何使用 GitHub Actions 将 Hugo 静态网站部署到 GitHub Pages。GitHub Actions 是 GitHub 提供的自动化功能，可以让您直接在 GitHub 存储库中创建自定义软件开发工作流程。通过使用 GitHub Actions，您可以轻松自动化构建和部署 Hugo 静态网站的过程。\n配置 GitHub Pages 设置 在使用 GitHub Actions 成功部署 Hugo 静态网站到 GitHub Pages 之前，您需要为您的项目配置 GitHub Pages 设置。\n转到您的 GitHub 项目页面，点击右上角的 \u0026ldquo;Settings\u0026rdquo; 选项卡。 滚动到 \u0026ldquo;Pages\u0026rdquo; 部分。 在 \u0026ldquo;Build and deployment\u0026rdquo; 设置中，找到 \u0026ldquo;Source\u0026rdquo; 下拉菜单。 从可用选项中选择 \u0026ldquo;GitHub Actions\u0026rdquo;。这告诉 GitHub Pages 使用由 GitHub Actions 生成的工件进行部署。 现在您已经配置了 GitHub Pages 设置，您的站点将使用 hugo.yaml 文件中定义的工作流进行部署。\n设置 GitHub Pages 工作流程 要使用 Hugo 和 GitHub Actions 设置 GitHub Pages 工作流程，请按照以下步骤进行操作：\n在 Hugo 站点存储库的 .github/workflows/ 目录中创建名为 hugo.yaml 的新文件。 将以下 YAML 配置复制到 hugo.yaml 文件中： name: Deploy Hugo site to Pages on: # Runs on pushes targeting the default branch push: branches: - main # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. concurrency: group: \u0026#34;pages\u0026#34; cancel-in-progress: false # Default to bash defaults: run: shell: bash jobs: # Build job build: runs-on: ubuntu-latest env: HUGO_VERSION: 0.111.3 steps: - name: Install Hugo CLI run: | wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \\ \u0026amp;\u0026amp; sudo dpkg -i ${{ runner.temp }}/hugo.deb - name: Install Dart Sass Embedded run: sudo snap install dart-sass-embedded - name: Checkout uses: actions/checkout@v3 with: submodules: recursive fetch-depth: 0 - name: Setup Pages id: pages uses: actions/configure-pages@v3 - name: Install Node.js dependencies run: \u0026#34;[[ -f package-lock.json || -f npm-shrinkwrap.json ]] \u0026amp;\u0026amp; npm ci || true\u0026#34; - name: Build with Hugo env: # For maximum backward compatibility with Hugo modules HUGO_ENVIRONMENT: production HUGO_ENV: production run: | hugo \\ --gc \\ --minify \\ --baseURL \u0026#34;${{ steps.pages.outputs.base_url }}/\u0026#34; - name: Upload artifact uses: actions/upload-pages-artifact@v1 with: path: ./public # Deployment job deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v2 此配置文件设置了一个工作流程，当您将代码推送到 main 分支或从 Actions 选项卡手动运行工作流程时，它将构建您的 Hugo 静态网站并将其部署到 GitHub Pages。\n结论 通过配置项目中的 GitHub Pages 设置并使用提供的 hugo.yaml 工作流配置，您可以轻松自动化构建和部署 Hugo 静态网站到 GitHub Pages 的过程。通过利用 GitHub Actions，您可以专注于创建内容和更新您的网站，而无需每次手动部署它。此设置还允许您利用 GitHub Actions 提供的内置 CI/CD 能力，进一步改进您的开发工作流程。\nSource code : https://github.com/hobbyworker/hugo-demo\nDemo : https://hobbyworker.me/hugo-demo\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-25-deploying-a-hugo-static-site-to-github-pages-with-github-actions/","summary":"\u003cp\u003e在本博客文章中，我们将讨论如何使用 GitHub Actions 将 Hugo 静态网站部署到 GitHub Pages。GitHub Actions 是 GitHub 提供的自动化功能，可以让您直接在 GitHub 存储库中创建自定义软件开发工作流程。通过使用 GitHub Actions，您可以轻松自动化构建和部署 Hugo 静态网站的过程。\u003c/p\u003e","title":"将 Hugo 静态网站使用 GitHub Actions 部署到 GitHub Pages"},{"content":"在本博客文章中，我们将学习如何使用 PaperMod 主题为您的 Hugo 博客添加广告拦截检测。我们还将包括一个简单的警告消息，提示已启用广告拦截器的用户禁用它或将您的网站添加到白名单。\n概述 以下是我们将采取的步骤的快速概述：\n创建自定义 CSS 文件来样式化警告消息。 创建 JavaScript 文件以检测广告拦截器。 添加部分 HTML 文件以显示警告消息。 扩展 head 和 footer 部分以包含我们的新文件。 逐步指南 1. 创建自定义 CSS 文件 在 assets/css/extended/ 目录下创建一个名为 custom_css.css 的新文件，并粘贴以下 CSS 代码：\n#adblock-warning { background-color: #f2dede; color: #a94442; border-color: #ebccd1; padding: 15px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; } 2. 创建 JavaScript 文件以检测广告拦截器 在 static/js/ 目录下创建一个名为 adblock-detection.js 的新文件，并粘贴以下 JavaScript 代码：\nfunction detectAdBlocker() { const adBlockTest = document.createElement(\u0026#39;div\u0026#39;); adBlockTest.innerHTML = \u0026#39;\u0026amp;nbsp;\u0026#39;; adBlockTest.className = \u0026#39;ad ads adbadge doubleclick BannerAd adsbox ad-placeholder ad-placement\u0026#39;; document.body.appendChild(adBlockTest); window.setTimeout(() =\u0026gt; { if (adBlockTest.offsetHeight === 0) { document.getElementById(\u0026#39;adblock-warning\u0026#39;).style.display = \u0026#39;block\u0026#39;; console.log(\u0026#39;Ad-blocker detected.\u0026#39;); } else { console.log(\u0026#39;No ad-blocker detected.\u0026#39;); } adBlockTest.remove(); }, 100); } window.addEventListener(\u0026#39;load\u0026#39;, detectAdBlocker); 3. 添加用于警告消息的部分 HTML 文件 在 layouts/partials/ 目录下创建一个名为 adblock-warning.html 的新文件，并粘贴以下 HTML 代码：\n\u0026lt;div id=\u0026#34;adblock-warning\u0026#34; style=\u0026#34;display:none;\u0026#34;\u0026gt; \u0026lt;p\u0026gt;Please consider disabling your ad-blocker or whitelisting our website. Ads help support our content and keep it free for everyone. Thank you!\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; 4. 扩展 head 和 footer 部分以包含我们的新文件 编辑 layouts/partials/extend_head.html 文件，并添加以下行：\n\u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;{{ \u0026#34;css/extended/custom_css.css\u0026#34; | relURL }}\u0026#34;\u0026gt; 编辑 layouts/partials/extend_footer.html 文件，并添加以下行：\n\u0026lt;script src=\u0026#34;{{ \u0026#34;js/adblock-detection.js\u0026#34; | relURL }}\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; 结论 通过完成以上步骤，您已成功地使用 PaperMod 主题为您的 Hugo 博客添加了广告拦截检测。启用广告拦截器的访问者现在将看到一个礼貌的警告消息，鼓励他们禁用广告拦截器或将您的网站添加到白名单，帮助支持您的内容并使其对所有人免费。\nSource code : https://github.com/hobbyworker/hugo-demo\nDemo : https://hobbyworker.me/hugo-demo\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-24-adding-adblocker-detection-to-your-hugo-blog-with-papermod-theme/","summary":"\u003cp\u003e在本博客文章中，我们将学习如何使用 PaperMod 主题为您的 Hugo 博客添加广告拦截检测。我们还将包括一个简单的警告消息，提示已启用广告拦截器的用户禁用它或将您的网站添加到白名单。\u003c/p\u003e","title":"为您的 Hugo 博客使用 PaperMod 主题添加广告拦截检测"},{"content":"在本篇博客文章中，我们将学习如何将nvm（Node版本管理器）和autoenv结合起来，以管理Node.js版本和环境变量，以适应您的开发工作流程。本指南假定您已在系统上安装了nvm和autoenv。\n为什么要同时使用NVM和Autoenv？ nvm是一个非常棒的工具，可以在您的系统上管理多个Node.js版本，让您轻松地在它们之间切换。autoenv简化了管理环境变量的过程，当您进入一个目录时，它会自动从.env文件中加载它们。\n通过结合使用这两个工具，您可以设置开发环境，以自动切换到适当的Node.js版本并加载相关的环境变量，简化您的工作流程。\n创建.env文件 首先，在您的项目根目录中创建一个.env文件。此文件将包含项目中要使用的环境变量和Node.js版本。\n以下是.env文件可能看起来像的示例：\nexport NODE_ENV=development export API_KEY=your_api_key_here export PORT=3000 export NVM_DIR=\u0026#34;$HOME/.nvm\u0026#34; nvm use 14.17.0 在这个示例中，我们设置了NODE_ENV，API_KEY和PORT环境变量。我们还指定了nvm目录的路径，并指示它在项目中使用Node.js版本14.17.0。\n使用NVM和Autoenv 现在您已经设置好了.env文件，您需要配置autoenv以与nvm配合使用。为此，请根据您的shell在.autoenv.zsh或.autoenv.sh文件中添加以下行：\nsource \u0026#34;$NVM_DIR/nvm.sh\u0026#34; 这一行确保在autoenv加载.env文件时，nvm命令是可用的。\n设置您的项目 配置完成后，使用终端导航到您的项目根目录。您应该会看到来自autoenv的消息，指示它已加载.env文件：\n$ cd your_project_directory autoenv: autoenv: Loading .env autoenv: Switching to Node.js v14.17.0 现在，指定的Node.js版本和来自.env文件的环境变量将自动设置为您的项目。\n在项目之间切换 当您在具有不同.env文件的项目之间导航时，autoenv和nvm将自动相应地调整Node.js版本和环境变量：\n$ cd another_project_directory autoenv: autoenv: Loading .env autoenv: Switching to Node.js v12.22.1 这使得管理不同的Node.js版本和环境变量变得轻而易举！\n结论 通过结合使用nvm和autoenv，您可以极大地简化管理项目的Node.js版本和环境变量的过程。这将使您的开发过程更加高效，并确保您始终使用每个项目的正确设置。\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-23-using-nvm-and-autoenv-in-combination/","summary":"\u003cp\u003e在本篇博客文章中，我们将学习如何将\u003ccode\u003envm\u003c/code\u003e（Node版本管理器）和\u003ccode\u003eautoenv\u003c/code\u003e结合起来，以管理Node.js版本和环境变量，以适应您的开发工作流程。本指南假定您已在系统上安装了\u003ccode\u003envm\u003c/code\u003e和\u003ccode\u003eautoenv\u003c/code\u003e。\u003c/p\u003e","title":"结合使用NVM和Autoenv"},{"content":"在这篇博客文章中，我们将讨论如何使用jEnv和autoenv结合使用来管理多个Java版本并自动设置项目的环境变量。本教程假设您已经在系统上安装了jEnv和autoenv。我们将提供一些工作示例来演示它们的用法。\njEnv和autoenv概述 jEnv jEnv是一个命令行工具，简化了在系统上管理多个Java安装的过程。它允许您轻松切换不同的Java版本，设置全局或本地版本，并提供一种方便的方式来配置您的环境。\nautoenv autoenv是一个命令行工具，当您导航到项目目录时自动设置环境变量。它通过查找项目目录中的.env文件并执行其内容来工作。\n配置jEnv 在我们深入示例之前，让我们简要介绍如何配置jEnv。首先，使用jenv add命令将安装的Java版本添加到jEnv中：\njenv add /path/to/java/version 要列出所有添加的Java版本，请使用：\njenv versions 要设置全局Java版本，请使用：\njenv global \u0026lt;version\u0026gt; 要为特定项目设置本地Java版本，请导航到该项目目录并使用：\njenv local \u0026lt;version\u0026gt; 使用jEnv和autoenv 现在，让我们看一些结合使用jEnv和autoenv的工作示例。\nExample 1: 为项目设置Java版本 在您的项目目录中创建一个.env文件，并包含以下内容：\n# .env export JAVA_HOME=$(jenv prefix) 当您导航到该项目目录时，autoenv将自动将JAVA_HOME环境变量设置为由jEnv配置的当前活动的Java版本。\nExample 2: 设置Java版本和其他环境变量 假设您有一个需要特定Java版本和其他一些环境变量的项目。在项目目录中创建一个.env文件，并包含以下内容：\n# .env jenv local \u0026lt;version\u0026gt; export JAVA_HOME=$(jenv prefix) export APP_ENV=development export API_KEY=your_api_key 当您导航到该项目目录时，autoenv将自动设置本地Java版本、JAVA_HOME和其他所需的环境变量。\nExample 3: 在离开项目目录时取消设置环境变量 要在离开项目目录时取消设置环境变量，请在项目目录中创建一个.env.leave文件，并包含以下内容：\n# .env.leave unset JAVA_HOME unset APP_ENV unset API_KEY 现在，当您导航出项目目录时，autoenv将自动取消设置环境变量。\n结论 通过结合使用jEnv和autoenv，您可以简化Java版本和环境变量的管理。这有助于创建更一致和可预测的开发环境，使您可以更轻松地处理具有不同Java版本和配置的多个项目。此外，这些工具自动化了在各种Java环境之间切换的过程，从而减少了手动配置所需的时间，并最小化了潜在的错误。\n此外，通过利用jEnv和autoenv的强大功能，开发人员可以更有效地与团队合作，因为设置过程变得更加简化和可重复。这使新团队成员的入职更加顺畅，并促进了协同开发流程。\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-22-how-to-use-jenv-and-autoenv-in-combination/","summary":"\u003cp\u003e在这篇博客文章中，我们将讨论如何使用\u003ccode\u003ejEnv\u003c/code\u003e和\u003ccode\u003eautoenv\u003c/code\u003e结合使用来管理多个Java版本并自动设置项目的环境变量。本教程假设您已经在系统上安装了\u003ccode\u003ejEnv\u003c/code\u003e和\u003ccode\u003eautoenv\u003c/code\u003e。我们将提供一些工作示例来演示它们的用法。\u003c/p\u003e","title":"如何结合使用jEnv和autoenv"},{"content":"在这篇博文中，我们将讨论如何结合rbenv和autoenv的力量，在你的项目中无缝管理Ruby版本和环境变量。在本篇博文结束时，你将清楚地了解如何一起使用这些工具，使你的Ruby开发经验更加完善。\n注意： 本教程假定你已经安装了rbenv和autoenv。如果你还没有这样做，请按照rbenv和autoenv的安装说明。\n为什么要同时使用rbenv和autoenv？ rbenv是一个强大的工具，可以让你轻松管理多个Ruby版本。另一方面，autoenv帮助你管理特定于项目目录的环境变量。通过同时使用这两个工具，你可以确保你在每个项目中使用正确的Ruby版本和环境变量，而不需要手动干预。\n配置 rbenv 和 autoenv 在深入研究例子之前，让我们先配置autoenv，使其与rbenv一起工作。要做到这一点，在你的项目目录下创建一个新的.env文件，并添加以下几行：\nexport RBENV_VERSION=$(cat .ruby-version) export PATH=\u0026#34;$HOME/.rbenv/shims:$PATH\u0026#34; 第一行根据.ruby-version文件的内容设置RBENV_VERSION环境变量。第二行确保rbenv shims目录在PATH中，这样在运行Ruby命令时就会使用正确的Ruby版本。\nExample 1: 切换Ruby版本 假设你有两个项目： project_a和project_b。你想对project_a使用Ruby 2.7.4版本，对project_b使用Ruby 3.0.2版本。下面是你如何使用rbenv和autoenv来实现这个目标：\n在每个项目目录下创建一个.ruby-version文件： echo \u0026#34;2.7.4\u0026#34; \u0026gt; project_a/.ruby-version echo \u0026#34;3.0.2\u0026#34; \u0026gt; project_b/.ruby-version 在每个项目目录下创建一个.env文件，内容如前所述： cp .env project_a/ cp .env project_b/ 现在, 当你导航到project_a或project_b时, autoenv会自动设置RBENV_VERSION并调整PATH以使用正确的Ruby版本.\nExample 2: 管理特定项目的环境变量 假设project_a需要以下环境变量：\nAPI_KEY: 你的第三方服务的API密钥 SECRET_KEY: 用于加密数据的密匙 你可以像这样把这些变量添加到project_a的.env文件中：\nexport RBENV_VERSION=$(cat .ruby-version) export PATH=\u0026#34;$HOME/.rbenv/shims:$PATH\u0026#34; export API_KEY=\u0026#34;your_api_key_here\u0026#34; export SECRET_KEY=\u0026#34;your_secret_key_here\u0026#34; 现在，当你导航到project_a时，autoenv会自动设置RBENV_VERSION，调整PATH，并设置API_KEY和SECRET_KEY环境变量。\n结论 通过一起使用rbenv和autoenv，你可以轻松地管理Ruby版本和项目特定的环境变量，而无需任何人工干预。这种组合使你的Ruby开发经验更有效率，更少出错。\n不要忘记将你的.env文件添加到你的.gitignore中，以避免意外地将敏感信息提交给你的版本控制系统。\n有了这些设置，你将享受到为你的每个项目量身定做的流畅的Ruby开发工作流程。编码愉快!\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-21-how-to-use-rbenv-and-autoenv-in-combination/","summary":"\u003cp\u003e在这篇博文中，我们将讨论如何结合\u003ccode\u003erbenv\u003c/code\u003e和\u003ccode\u003eautoenv\u003c/code\u003e的力量，在你的项目中无缝管理Ruby版本和环境变量。在本篇博文结束时，你将清楚地了解如何一起使用这些工具，使你的Ruby开发经验更加完善。\u003c/p\u003e","title":"如何结合使用rbenv和autoenv"},{"content":"I. 概述 在这篇博文中，我们将探讨如何使用pyenv-virtualenv和autoenv来实现无缝的Python开发。这些工具可以帮助你轻松地管理多个Python环境和虚拟环境，改善你的开发工作流程。\nII. Pyenv-virtualenv Pyenv-virtualenv是pyenv的一个插件，允许你为不同的Python版本创建和管理虚拟环境。它可以帮助你把不同项目的依赖关系分开，确保每个项目都能访问它所需要的包，而不受干扰。\n创建一个虚拟环境 要使用pyenv-virtualenv创建一个新的虚拟环境，请使用以下命令：\npyenv virtualenv \u0026lt;python-version\u0026gt; \u0026lt;virtualenv-name\u0026gt; 例如，如果你想用Python 3.8.0创建一个名为my_project的虚拟环境：\npyenv virtualenv 3.8.0 my_project 列出虚拟环境 要列出所有你已经创建的虚拟环境，使用下面的命令：\npyenv virtualenvs 激活一个虚拟环境 要激活一个虚拟环境，使用下面的命令：\npyenv activate \u0026lt;virtualenv-name\u0026gt; 例如：\npyenv activate my_project 停用一个虚拟环境 要停用当前的虚拟环境, 使用以下命令：\npyenv deactivate III. Autoenv Autoenv是一个工具，当你进入一个包含.env文件的目录时，它会自动激活一个虚拟环境。这使得在项目之间的切换很容易，而不需要记住手动激活和停用虚拟环境。\n设置Autoenv 为了使用 autoenv，你需要在项目的根目录下创建一个 .env 文件。这个文件将包含当你进入该目录时应该执行的命令。\n例如，假设你有一个位于~/projects/my_project的项目，你想使用之前创建的my_project虚拟环境。在~/projects/my_project目录下创建一个.env文件，内容如下：\nsource $(pyenv root)/versions/my_project/bin/activate 使用Autoenv 现在, 当你导航到项目目录时, autoenv会自动为你激活my_project虚拟环境：\ncd ~/projects/my_project 你应该看到一条信息，表明虚拟环境已经被激活：\nautoenv: Activating environment . . . (my_project) $ 当你离开项目目录时，虚拟环境将被自动停用：\ncd ~ 你应该看到一条消息，表明虚拟环境已经被停用：\nautoenv: Deactivating environment . . . $ IV. 总结 通过结合pyenv-virtualenv和autoenv，你可以创建一个无缝的开发工作流程来管理多个Python项目。这种方法确保你总是为每个项目使用正确的虚拟环境，同时也保持你的依赖关系的分离和组织。\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-20-using-pyenv-virtualenv-and-autoenv-in-combination-for-python-development/","summary":"\u003ch1 id=\"i-概述\"\u003eI. 概述\u003c/h1\u003e\n\u003cp\u003e在这篇博文中，我们将探讨如何使用\u003ccode\u003epyenv-virtualenv\u003c/code\u003e和\u003ccode\u003eautoenv\u003c/code\u003e来实现无缝的Python开发。这些工具可以帮助你轻松地管理多个Python环境和虚拟环境，改善你的开发工作流程。\u003c/p\u003e","title":"将Pyenv-virtualenv和Autoenv结合起来用于Python开发"},{"content":"I. 概述 Autoenv是一个让你轻松管理环境变量的工具。有了它，你可以在进入一个目录时自动设置环境变量，并在离开时取消设置。这使得它成为一个强大的工具，可以为不同的项目管理不同的环境，并使重复性任务自动化。在这篇文章中，我们将介绍如何在不同的平台上安装Autoenv以及一些使用实例。\nII. 安装 Mac Autoenv可以通过Homebrew轻松地安装在macOS上。首先，确保你已经安装了Homebrew。然后，运行以下命令：\nbrew install autoenv Linux Autoenv可以用apt-get安装在Linux系统上。运行以下命令：\nsudo apt-get install autoenv Windows Autoenv可以通过Git Bash或WSL在Windows上安装。你可以从Git网站下载Git Bash或者在Windows 10上启用WSL。安装完毕后，你可以按照Linux的安装说明进行安装。\nIII. 使用实例 一旦Autoenv安装完毕，你可以用它来管理不同项目的环境变量。让我们来看看一些例子。\nExample 1: 设置环境变量 假设你正在做一个需要设置特定环境变量的Python项目。你可以在项目目录下创建一个.env文件，内容如下：\nexport API_KEY=my_api_key export DATABASE_URL=postgres://user:password@localhost/mydatabase 现在，当你进入项目目录时，Autoenv将自动设置这些环境变量。你可以使用os.environ从你的Python代码中访问它们。\nExample 2: 在进入目录时运行命令 你也可以配置 Autoenv 在你进入一个目录时运行命令。例如，假设你总是想在进入一个项目目录时激活一个虚拟环境。你可以在 .env 文件中添加以下一行：\nsource venv/bin/activate 现在，当你进入项目目录的时候，Autoenv会自动激活虚拟环境。\nExample 3: 取消设置环境变量 当你离开一个目录时，Autoenv也可以取消你设置的环境变量。这对于清理自己的工作和确保环境变量不被泄露到其他项目中是很有用的。要做到这一点，请在 .env 文件中添加以下一行：\nunset API_KEY unset DATABASE_URL 现在，当你离开项目目录时，Autoenv将自动取消环境变量的设置。\nIV. 总结 Autoenv是一个管理环境变量和自动化重复性任务的强大工具。有了它，你可以轻松地设置和取消环境变量，在目录条目上运行命令，等等。通过在你的系统上安装Autoenv并在你的项目中使用它，你可以节省时间并简化你的工作流程。\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-19-using-autoenv-the-ultimate-shortcut-to-environment-management/","summary":"\u003ch1 id=\"i-概述\"\u003eI. 概述\u003c/h1\u003e\n\u003cp\u003eAutoenv是一个让你轻松管理环境变量的工具。有了它，你可以在进入一个目录时自动设置环境变量，并在离开时取消设置。这使得它成为一个强大的工具，可以为不同的项目管理不同的环境，并使重复性任务自动化。在这篇文章中，我们将介绍如何在不同的平台上安装Autoenv以及一些使用实例。\u003c/p\u003e","title":"使用Autoenv：环境管理的终极捷径"},{"content":"I. 概述 Node版本管理器（NVM）是一个管理和切换多个Node.js版本的有用工具。在这篇博文中，我们将介绍NVM最常用的功能，不同平台的安装说明，以及为什么它是一个对开发者有价值的工具。\nII. 安装 macOS 使用Homebrew：\nbrew install nvm mkdir ~/.nvm 在你的.bash_profile, .zshrc, 或其他shell配置文件中添加以下几行：\nexport NVM_DIR=\u0026#34;$HOME/.nvm\u0026#34; [ -s \u0026#34;$(brew --prefix)/opt/nvm/nvm.sh\u0026#34; ] \u0026amp;\u0026amp; . \u0026#34;$(brew --prefix)/opt/nvm/nvm.sh\u0026#34; # This loads nvm [ -s \u0026#34;$(brew --prefix)/opt/nvm/etc/bash_completion\u0026#34; ] \u0026amp;\u0026amp; . \u0026#34;$(brew --prefix)/opt/nvm/etc/bash_completion\u0026#34; # This loads nvm bash_completion Linux和其他基于Unix的系统 使用curl：\ncurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash 或者使用wget：\nwget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash 这个脚本将克隆NVM仓库到~/.nvm，并在你的shell配置文件（.bashrc，.zshrc，等等）中添加必要的行。\nIII. 使用方法 1. 列出可用的Node.js版本 要查看可用的Node.js版本列表，请运行：\nnvm ls-remote 2. 安装一个特定的Node.js版本 要安装一个特定的版本，请使用nvm install命令，后面加上版本号：\nnvm install 14.17.0 3. 列出已安装的Node.js版本 要查看已安装的Node.js版本列表，请运行：\nnvm ls 4. 在Node.js版本之间进行切换 要切换到一个特定的Node.js版本，请使用nvm use命令，后面加上版本号：\nnvm use 14.17.0 5. 设置一个默认的Node.js版本 要为新的shell会话设置一个默认版本，请使用nvm alias命令：\nnvm alias default 14.17.0 6. 卸载一个Node.js版本 要卸载一个特定的Node.js版本，请使用nvm uninstall命令，后面加上版本号：\nnvm uninstall 14.17.0 7. 安装最新的LTS（长期支持）版本 要安装最新的LTS版本，请运行：\nnvm install --lts 8. 更新已安装的Node.js版本 要将已安装的版本更新为最新的补丁，请使用nvm reinstall-packages命令：\nnvm install 14.17.0 --reinstall-packages-from=14.16.0 9. 使用特定的Node.js版本运行脚本 要使用特定的Node.js版本运行脚本而不切换活动版本，可以使用nvm exec命令：\nnvm exec 14.17.0 node script.js 10. 使用特定的Node.js版本运行一个脚本 要使用特定的Node.js版本运行一个命令而不切换活动版本，请使用nvm run命令：\nnvm run 14.17.0 --version IV. 总结 NVM是一个强大的工具，允许开发人员轻松地管理多个Node.js版本。它能够在Node.js版本之间轻松切换，使得在不同环境中测试应用程序或在具有不同Node.js要求的多个项目中工作变得简单。\n通过这篇博文中所涉及的最常用的功能，你现在应该能够在你的系统上安装NVM，管理Node.js版本，并有效地使用该工具。编码愉快!\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-18-managing-multiple-nodejs-environments-with-nvm/","summary":"\u003ch1 id=\"i-概述\"\u003eI. 概述\u003c/h1\u003e\n\u003cp\u003eNode版本管理器（NVM）是一个管理和切换多个Node.js版本的有用工具。在这篇博文中，我们将介绍NVM最常用的功能，不同平台的安装说明，以及为什么它是一个对开发者有价值的工具。\u003c/p\u003e","title":"用NVM管理多个Node.js环境"},{"content":"Java是一种非常流行的编程语言，而管理多个Java版本可能是一项具有挑战性的任务。这就是jEnv的用武之地。在这篇博文中，我们将介绍jEnv最常用的功能，包括各种平台的安装说明，并帮助你轻松地管理你的Java版本。\nI. 概述 jEnv是一个命令行工具，使你能够管理你系统上的多个Java版本。它允许你为每个项目、shell会话或整个系统设置你所需要的Java版本。通过使用jEnv，你可以避免需要不同Java版本的不同项目之间的冲突，确保更顺利的开发体验。\nII. 安装 Mac 要在macOS上安装jEnv，你可以使用Homebrew软件包管理器。如果你还没有安装Homebrew，你可以找到安装说明 这里。\nbrew install jenv 安装后，在你的~/.bash_profile、~/.zshrc或~/.bashrc文件中添加以下几行，取决于你的shell：\nexport PATH=\u0026#34;$HOME/.jenv/bin:$PATH\u0026#34; eval \u0026#34;$(jenv init -)\u0026#34; Linux 要在Linux上安装jEnv，执行以下命令：\ngit clone https://github.com/jenv/jenv.git ~/.jenv 克隆仓库后，根据你的shell，在你的~/.bashrc或~/.zshrc文件中添加以下几行：\nexport PATH=\u0026#34;$HOME/.jenv/bin:$PATH\u0026#34; eval \u0026#34;$(jenv init -)\u0026#34; III. 使用方法 local local命令为一个特定的目录设置Java版本。当在多个有不同Java版本要求的项目上工作时，这很有用。\njenv local 11.0.2 global global命令为整个系统设置默认的Java版本。如果 local 或 shell 命令没有指定其他版本，就会使用这个版本。\njenv global 11.0.2 shell shell命令为当前的shell会话设置Java版本。当你想暂时使用一个不同的Java版本而不影响其他项目或会话时，这很有用。\njenv shell 11.0.2 rehash rehash命令为jEnv已知的所有Java可执行文件生成垫片。这在安装新的Java版本后或jEnv不能识别某些Java可执行文件时很有用。\njenv rehash version version命令显示当前活动的Java版本。\njenv version versions versions命令列出所有已安装的Java版本，并用星号标记当前活动的版本。\njenv versions which which命令显示当前活动版本的Java可执行文件的路径。\njenv which java whence whence命令列出包含特定命令的所有Java版本。\njenv whence java add add命令在jEnv中注册了一个新的Java版本。你需要提供Java安装目录的路径。\njenv add /path/to/java/home IV. 总结 jEnv是一个强大的工具，简化了Java版本管理。通过local、global和shell等功能，你可以在每个项目或每个会话的基础上轻松切换Java版本。rehash、version、versions、which、whence和add命令进一步增强了你管理和组织Java安装的能力。通过使用jEnv，你可以简化你的开发工作流程，避免版本冲突，并确保你的项目运行在理想的Java版本上。无论你是在处理多个Java项目，还是仅仅想对你的Java环境有更大的控制，jEnv都是Java开发者不可或缺的工具。\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-17-a-practical-guide-to-using-jenv-for-java-version-management/","summary":"\u003cp\u003eJava是一种非常流行的编程语言，而管理多个Java版本可能是一项具有挑战性的任务。这就是jEnv的用武之地。在这篇博文中，我们将介绍jEnv最常用的功能，包括各种平台的安装说明，并帮助你轻松地管理你的Java版本。\u003c/p\u003e","title":"使用jEnv进行Java版本管理的实用指南"},{"content":"在这篇博文中，我们将讨论如何使用pyenv和pyenv-virtualenv，这是两个强大的工具，可以帮助你轻松地管理多个Python版本和虚拟环境。我们将介绍各种平台的安装说明，如Mac和Linux，并讨论这些工具最经常使用的功能。到最后，你将对如何在你的开发工作流程中有效使用这些工具有一个坚实的了解。\nI. 概述 pyenv是一个强大的Python版本管理工具，允许你轻松地安装和切换多个Python版本。pyenv-virtualenv是pyenv的一个扩展，使你能够管理多个虚拟环境。这些工具在处理具有不同依赖性和Python版本的多个项目时特别有用。\nII. 安装 Mac 要在macOS上安装pyenv和pyenv-virtualenv，你可以使用 Homebrew：\nbrew update brew install pyenv brew install pyenv-virtualenv 安装完成后，在你的shell配置文件（.bashrc, .zshrc, etc.）中添加以下几行：\nif command -v pyenv 1\u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then eval \u0026#34;$(pyenv init -)\u0026#34; fi if command -v pyenv-virtualenv-init 1\u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then eval \u0026#34;$(pyenv virtualenv-init -)\u0026#34; fi Linux 要在Linux上安装 pyenv 和 pyenv-virtualenv，首先克隆软件库并将其添加到你的 \u0026ldquo;PATH\u0026rdquo;：\ngit clone https://github.com/pyenv/pyenv.git ~/.pyenv git clone https://github.com/pyenv/pyenv-virtualenv.git ~/.pyenv/plugins/pyenv-virtualenv 接下来，在你的shell配置文件（.bashrc, .zshrc, etc.）中添加以下几行：\nexport PYENV_ROOT=\u0026#34;$HOME/.pyenv\u0026#34; export PATH=\u0026#34;$PYENV_ROOT/bin:$PATH\u0026#34; eval \u0026#34;$(pyenv init -)\u0026#34; eval \u0026#34;$(pyenv virtualenv-init -)\u0026#34; III. 使用方法 1. 安装一个Python版本 要安装一个特定的Python版本，使用install命令：\npyenv install 3.9.5 2. 列出可用的Python版本 要查看所有已安装的Python版本，使用versions命令：\npyenv versions 3. 设置全局Python版本 要设置全局Python版本，使用global命令：\npyenv global 3.9.5 4. 设置本地Python版本 要为一个特定的项目设置本地Python版本，请在项目目录下使用local命令：\npyenv local 3.8.10 5. 检查当前的Python版本 要检查当前的Python版本，使用version命令：\npyenv version 6. 创建一个虚拟环境 要用pyenv-virtualenv创建一个新的虚拟环境，请使用virtualenv命令：\npyenv virtualenv 3.9.5 my-project-env 7. 激活一个虚拟环境 要激活一个虚拟环境, 使用activate命令：\npyenv activate my-project-env 8. 停用一个虚拟环境 要停用当前的虚拟环境，使用deactivate命令：\npyenv deactivate 9. 列出可用的虚拟环境 要列出所有你已经创建的虚拟环境，使用virtualenvs命令：\npyenv virtualenvs 10. 删除一个虚拟环境 要删除一个虚拟环境，使用uninstall命令：\npyenv uninstall my-project-env Bonus: Rehash 每当你安装一个带有可执行脚本的新Python包时，必须运行rehash命令来更新垫片，确保新的脚本是可用的：\npyenv rehash IV. 总结 pyenv和pyenv-virtualenv是在开发工作流程中管理多个Python版本和虚拟环境的宝贵工具。有了本指南所讨论的功能，你将有足够的能力在多个项目上工作，并有不同的依赖性和Python版本。拥抱这些工具，保持一个干净和有组织的开发环境，提高你的生产力，减少依赖性冲突的风险。\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-16-how-to-use-pyenv-and-pyenv-virtualenv/","summary":"\u003cp\u003e在这篇博文中，我们将讨论如何使用\u003ccode\u003epyenv\u003c/code\u003e和\u003ccode\u003epyenv-virtualenv\u003c/code\u003e，这是两个强大的工具，可以帮助你轻松地管理多个Python版本和虚拟环境。我们将介绍各种平台的安装说明，如Mac和Linux，并讨论这些工具最经常使用的功能。到最后，你将对如何在你的开发工作流程中有效使用这些工具有一个坚实的了解。\u003c/p\u003e","title":"如何使用Pyenv和Pyenv-Virtualenv"},{"content":"Ruby开发者经常需要处理多个项目，每个项目都有自己特定的版本要求。对一个多功能且易于使用的版本管理器的需求是至关重要的。在这篇博文中，我们将讨论rbenv，一个流行的Ruby环境管理器，为这个问题提供一个优雅的解决方案。我们将讨论最常用的功能，各种平台的安装说明，并以结论作为结束。\nI. 概述 rbenv是一个轻量级的Ruby版本管理工具，它允许你在每个项目的基础上或在你的系统中全局地切换不同的Ruby版本。通过rbenv，你可以轻松地安装新的Ruby版本，保持它们的更新，并为每个版本维护孤立的gem sets。\nrbenv的一些最常用的功能包括：\n安装Ruby版本 设置全局Ruby版本 设置本地（项目专用）Ruby版本 列出已安装的Ruby版本 删除Ruby版本 II. 安装 Mac 要在macOS上安装rbenv，你可以使用[Homebrew]（https://brew.sh/）：\nbrew install rbenv 安装完成后，在bash中添加rbenv，以便每次打开终端时都能加载它：\necho \u0026#39;if which rbenv \u0026gt; /dev/null; then eval \u0026#34;$(rbenv init -)\u0026#34;; fi\u0026#39; \u0026gt;\u0026gt; ~/.zshrc source ~/.zshrc Linux 要在基于Linux的系统上安装rbenv，请遵循以下步骤：\n更新你的包裹清单： sudo apt-get update 安装依赖性： sudo apt-get install -y build-essential libssl-dev libreadline-dev zlib1g-dev 从其GitHub仓库克隆rbenv： git clone https://github.com/rbenv/rbenv.git ~/.rbenv 在你的PATH中加入rbenv： echo \u0026#39;export PATH=\u0026#34;$HOME/.rbenv/bin:$PATH\u0026#34;\u0026#39; \u0026gt;\u0026gt; ~/.bashrc 在你的shell中加入rbenv初始化： echo \u0026#39;eval \u0026#34;$(rbenv init -)\u0026#34;\u0026#39; \u0026gt;\u0026gt; ~/.bashrc 重新启动你的外壳： exec $SHELL III. 使用方法 1. 安装Ruby版本 要安装一个特定的Ruby版本，首先安装ruby-build插件：\nbrew install ruby-build 现在你可以安装所需的Ruby版本：\nrbenv install 2.7.0 2. 设置全局的Ruby版本 要为你的系统设置全局Ruby版本，请使用global命令：\nrbenv global 2.7.0 3. 设置本地（项目专用）Ruby版本 要为一个特定的项目设置Ruby版本，请导航到项目目录并使用local命令：\ncd /path/to/your/project rbenv local 2.7.0 4. 列出已安装的Ruby版本 要列出所有安装的Ruby版本，请使用versions命令：\nrbenv versions 5. 删除Ruby版本 要删除已安装的Ruby版本，请使用uninstall命令：\nrbenv uninstall 2.7.0 IV. 总结 总之，对于需要管理多个Ruby环境的Ruby开发者来说，rbenv是一个不可缺少的工具。它提供了一个简单而强大的方法来切换Ruby版本，管理宝石集，并确保项目特定的依赖关系得到维护。由于其易于安装和跨平台的兼容性，rbenv是任何希望简化其开发过程并保持其项目有序的Rubyist的必备工具。试试rbenv吧，你很快就会发现没有它你是怎么做到的。\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-15-managing-multiple-ruby-environments-with-rbenv/","summary":"\u003cp\u003eRuby开发者经常需要处理多个项目，每个项目都有自己特定的版本要求。对一个多功能且易于使用的版本管理器的需求是至关重要的。在这篇博文中，我们将讨论rbenv，一个流行的Ruby环境管理器，为这个问题提供一个优雅的解决方案。我们将讨论最常用的功能，各种平台的安装说明，并以结论作为结束。\u003c/p\u003e","title":"用 rbenv 管理多个 Ruby 环境"},{"content":"I. 简介 本开发报告旨在演示如何使用Notion的API来自动安排锻炼时间。Notion是一个多合一的生产力工具，允许用户创建和组织数据库、笔记、任务等等。Notion API为开发者提供了对Notion功能的编程访问，实现了各种任务的自动化。\n所提供的代码片段展示了如何使用Notion API在数据库中根据预定义的时间表创建锻炼事件。该脚本在一个特定的日期范围内获取现有的事件，并为没有任何预定事件的日子创建新的事件。锻炼计划是在 days_of_week 字典中定义的，它将一周中的每一天映射到锻炼名称和标签列表中。\n通过使用Notion API将锻炼安排过程自动化，开发者可以节省时间并减少人工错误。本报告将提供一个分步指南，帮助你设置Notion API并了解代码片段的各个组成部分。\nII. 设置Notion API 要使用Notion API，你需要有一个Notion账户和一个集成设置。这里有一个认证Notion API的分步指南。\n在Notion创建一个集成\n登录Notion，通过点击屏幕左下角的个人资料图标，然后点击 \u0026ldquo;集成\u0026rdquo;，进入集成页面。 点击 \u0026ldquo;创建一个新的集成 \u0026ldquo;按钮。 给你的集成一个名称，并选择你想使用它的工作区。 点击 \u0026ldquo;提交 \u0026ldquo;按钮，创建集成。 获取你的集成令牌\n一旦你的集成被创建，你可以通过点击集成的名称来获得你的集成令牌。 复制集成令牌，在你的代码中使用它。 安装Notion API软件包\n打开你的终端或命令提示符。\n运行以下命令来安装Notion API包。\npip install notion-client 设置Notion API客户端\n在你的代码中导入Notion API客户端，在顶部添加以下一行。\nfrom notion_client import Client 通过添加以下一行并将 \u0026lt;API Key\u0026gt; 替换为你的集成令牌来认证客户端。\nnotion = Client(auth=\u0026#34;\u0026lt;API Key\u0026gt;\u0026#34;) 设置数据库细节\n获得你要添加事件的数据库的数据库ID。 用你的数据库ID替换代码段中的 database_id 变量。 所提供的代码片段使用 notion_client.Client 对象与Notion API交互。 Client 对象用于验证API客户端，并提供对各种Notion资源的访问，如数据库和页面。\ndatabase_id 变量被设置为Notion数据库的ID，锻炼事件将被添加到该数据库。这个ID可以在数据库的URL中找到。\nIII. 定义锻炼计划 锻炼计划在代码片断中使用以下变量定义。\nstart_days_ago：在今天的日期之前的天数，以开始计划。 end_days_after : 今天的日期之后的天数，以结束该计划。 days_of_week : 一个字典，将一周中的每一天映射到锻炼名称和标签列表中。 days_of_week 字典用于定义一周中每一天的锻炼计划。一周中的每一天都是字典中的一个键，相应的值是另一个字典，包含锻炼名称和与锻炼相关的标签列表。比如说\ndays_of_week = { \u0026#34;Monday\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;DAY 1\u0026#34;, \u0026#34;tag\u0026#34;: [\u0026#34;chest\u0026#34;, \u0026#34;triceps\u0026#34;]}, \u0026#34;Tuesday\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;DAY 2\u0026#34;, \u0026#34;tag\u0026#34;: [\u0026#34;back\u0026#34;, \u0026#34;biceps\u0026#34;]}, \u0026#34;Wednesday\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;DAY 3\u0026#34;, \u0026#34;tag\u0026#34;: [\u0026#34;lower body\u0026#34;, \u0026#34;shoulder\u0026#34;]}, \u0026#34;Thursday\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;DAY 1\u0026#34;, \u0026#34;tag\u0026#34;: [\u0026#34;chest\u0026#34;, \u0026#34;triceps\u0026#34;]}, \u0026#34;Friday\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;DAY 2\u0026#34;, \u0026#34;tag\u0026#34;: [\u0026#34;back\u0026#34;, \u0026#34;biceps\u0026#34;]}, \u0026#34;Saturday\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;DAY 3\u0026#34;, \u0026#34;tag\u0026#34;: [\u0026#34;lower body\u0026#34;, \u0026#34;shoulder\u0026#34;]}, } 在这个例子中，日程表被分为三天，每一天都有一个特定的锻炼和标签与之相关。\nstart_days_ago 和 end_days_after 变量用于定义锻炼计划的日期范围。 start_days_ago 指定在今天的日期之前多少天开始安排， end_days_after 指定在今天的日期之后多少天结束安排。\n这段代码使用 datetime 模块来获得今天的日期，并计算出锻炼计划的开始和结束日期。开始日期是通过从今天的日期中减去 start_days_ago 得到的，结束日期是通过在今天的日期中加上 end_days_after 得到的。日期的格式为 %Y-%m-%d 格式的字符串。\nIV. 查询现有的事件 在创建新的锻炼事件之前，代码片段会在定义的日期范围内获取现有事件。这样做是为了避免在同一日期创建重复的事件。\nnotion.databases.query() 方法用于查询Notion数据库并检索现有事件。参数 filter 用于指定查询的过滤条件。在这种情况下，过滤器被设置为检索属于代码片段中定义的开始和结束日期的事件。下面是一个查询中使用的过滤器的例子。\n{ \u0026#34;and\u0026#34;: [ {\u0026#34;property\u0026#34;: \u0026#34;Date\u0026#34;, \u0026#34;date\u0026#34;: {\u0026#34;on_or_after\u0026#34;: start_date}}, {\u0026#34;property\u0026#34;: \u0026#34;Date\u0026#34;, \u0026#34;date\u0026#34;: {\u0026#34;on_or_before\u0026#34;: end_date}} ] } 该过滤器返回具有 Date 属性的事件，该属性位于 start_date 和 end_date 之间。\n查询响应的 results 属性被用来获取日期范围内的现有事件。结果被存储在 existing_events 变量中，以便以后在代码片段中使用。\nexisting_events = notion.databases.query( **{ \u0026#34;database_id\u0026#34;: database_id, \u0026#34;filter\u0026#34;: { \u0026#34;and\u0026#34;: [ {\u0026#34;property\u0026#34;: \u0026#34;Date\u0026#34;, \u0026#34;date\u0026#34;: {\u0026#34;on_or_after\u0026#34;: start_date}}, {\u0026#34;property\u0026#34;: \u0026#34;Date\u0026#34;, \u0026#34;date\u0026#34;: {\u0026#34;on_or_before\u0026#34;: end_date}} ] }, } ).get(\u0026#34;results\u0026#34;) V. 创建新事件 一旦获得日期范围内的现有事件，该代码段将继续为计划日期范围内的每一天创建新的锻炼事件。\n该代码片段使用 for 循环来迭代日期范围内的每一个日期。为每一个不是星期天的日子创建一个新的事件。\nfor date in (datetime.now() + timedelta(days=n) for n in range(-start_days_ago, end_days_after + 1)): # skip Sundays if date.strftime(\u0026#34;%A\u0026#34;) == \u0026#34;Sunday\u0026#34;: continue strftime() 方法用于从日期中获取工作日名称，如果工作日名称是 \u0026ldquo;星期日\u0026rdquo;，则循环跳过这一天，转到下一天。\n然后，该代码片断检查在当前日期是否已经存在一个事件。\nif any(event.get(\u0026#34;properties\u0026#34;).get(\u0026#34;Date\u0026#34;).get(\u0026#34;date\u0026#34;).get(\u0026#34;start\u0026#34;) == date.strftime(\u0026#34;%Y-%m-%d\u0026#34;) for event in existing_events): print(f\u0026#34;Event already exists for {date.strftime(\u0026#39;%Y-%m-%d\u0026#39;)}. Skipping.\u0026#34;) continue 如果当前日期已经有一个事件存在，循环就会跳过这一天，转到下一天。\n如果当前日期没有事件存在，则创建一个新的事件，其锻炼名称和标签与一周中的那一天相关。\nday_of_week = date.strftime(\u0026#34;%A\u0026#34;) event_name = days_of_week[day_of_week][\u0026#34;name\u0026#34;] event_tag = days_of_week[day_of_week][\u0026#34;tag\u0026#34;] new_event = { \u0026#34;Name\u0026#34;: {\u0026#34;title\u0026#34;: [{\u0026#34;text\u0026#34;: {\u0026#34;content\u0026#34;: event_name}}]}, \u0026#34;Date\u0026#34;: {\u0026#34;date\u0026#34;: {\u0026#34;start\u0026#34;: date.strftime(\u0026#34;%Y-%m-%d\u0026#34;)}}, \u0026#34;Tag\u0026#34;: {\u0026#34;multi_select\u0026#34;: [{\u0026#34;name\u0026#34;: tag} for tag in event_tag]}, } notion.pages.create(parent={\u0026#34;database_id\u0026#34;: database_id}, properties=new_event) 从 days_of_week 字典中获得与一周中的那一天相关的锻炼名称和标签。 new_event 字典用于定义新事件的属性，其中包括事件名称、日期和标签。 notion.pages.create() 方法用于在Notion数据库中创建新事件。\n通过在计划日期范围内为一周中的每一天创建新的事件，开发者可以将锻炼安排过程自动化，并确保锻炼计划是最新的和准确的。\nVI. 结论 在这个项目中，我们已经演示了如何使用Notion API来自动创建Notion数据库中的锻炼计划的过程。我们提供的代码片段可以用来创建一个特定日期范围的锻炼计划，并根据定义的锻炼计划自动向数据库中添加新的事件。\nNotion API可用于自动化各种任务，如管理待办事项清单、跟踪支出或组织项目任务。通过Notion API，开发者可以建立自定义集成，并将重复性任务自动化，从而节省时间并提高生产力。\n总的来说，Notion API为开发者提供了一个强大的工具，可以将Notion整合到他们的工作流程中，并使他们的任务自动化。随着Notion的不断普及，我们可以期待看到更多的开发者利用Notion API构建集成和自动化任务。\n在未来，我们计划通过增加更多的功能和与其他工具的集成来扩展这个项目。我们还计划探索Notion API的其他用例，看看如何使用它来自动化我们工作流程中的其他任务。\nSource code : https://github.com/hobbyworker/notion-workout-scheduler\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-14-automating-workout-scheduling-with-notion-api/","summary":"\u003ch1 id=\"i-简介\"\u003e\u003cstrong\u003eI. 简介\u003c/strong\u003e\u003c/h1\u003e\n\u003cp\u003e本开发报告旨在演示如何使用Notion的API来自动安排锻炼时间。Notion是一个多合一的生产力工具，允许用户创建和组织数据库、笔记、任务等等。Notion API为开发者提供了对Notion功能的编程访问，实现了各种任务的自动化。\u003c/p\u003e","title":"用Notion API实现锻炼计划的自动化"},{"content":"概述 Homebrew是一个免费的、开源的软件包管理器，简化了在macOS上安装和管理软件的过程。使用Homebrew，您可以轻松地安装、更新和删除在macOS应用商店中不可用的软件包。由于其简单易用，它被开发人员和高级用户广泛采用。\n安装指南 在安装Homebrew之前，请确保您具备以下先决条件：\n安装了最新版本的Xcode命令行工具的macOS设备 稳定的互联网连接 要安装Homebrew，请在Mac上打开终端并运行以下命令：\n/bin/bash -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\u0026#34; 该命令从Homebrew存储库下载安装脚本并执行。安装过程可能需要几分钟才能完成。完成后，您可以通过运行以下命令来验证Homebrew是否已安装：\nbrew --version 如果您看到显示了一个版本号，恭喜您！您已经成功地在macOS设备上安装了Homebrew。\n如何使用Homebrew Homebrew建立在“公式”（formulae）的概念上，它们实质上是安装软件包的指令。这些公式用Ruby编写，并存储在Homebrew的官方存储库“Homebrew/core”中。\n以下是一些基本的Homebrew命令，可帮助您入门：\n搜索软件包： 使用brew search \u0026lt;package-name\u0026gt;来搜索可用的软件包。将\u0026lt;package-name\u0026gt;替换为您要查找的软件包的名称。 安装软件包： 使用brew install \u0026lt;package-name\u0026gt;来安装所需的软件包。 卸载软件包： 使用brew uninstall \u0026lt;package-name\u0026gt;来删除已安装的软件包。 列出已安装的软件包： 使用brew list来显示所有已安装的软件包列表。 更新Homebrew： 使用brew update来将Homebrew及其公式更新到最新版本。 升级已安装的软件包： 使用brew upgrade来将所有已安装的软件包升级到最新版本。 Example: 安装\u0026rsquo;wget' 在本示例中，我们将安装wget，一个广泛使用的用于从互联网下载文件的命令行实用程序。为此，在终端中运行以下命令即可：\nbrew install wget Homebrew现在将下载并安装wget软件包。安装完成后，您可以通过键入wget，然后跟随所需的选项和要下载的文件的URL来使用wget。例如，要下载一个样本文件，您可以运行：\nwget https://example.com/sample-file.txt 这将从指定的URL下载sample-file.txt并将其保存到您当前的工作目录中。\n结论 Homebrew是一个强大且易于使用的软件包管理器，填补了macOS软件管理能力中的重要空白。它简化了安装、更新和删除在App Store中不可用的开源软件和工具的过程。凭借其简单直观的命令行界面和庞大的公式存储库，Homebrew已成为macOS用户（尤其是开发人员和高级用户）必备的工具。\n在本文中，我们介绍了Homebrew的基础知识，包括如何安装、使用以及安装wget实用程序的实际示例。通过按照提供的说明和命令，您现在可以自信地探索和安装各种软件包，以增强您的macOS体验。祝您愉快地使用Homebrew！\n","permalink":"https://hobbyworker.me/zh-hans/dev/2023-03-13-a-beginners-guide-to-homebrew-the-missing-package-manager-for-macos/","summary":"\u003ch1 id=\"概述\"\u003e概述\u003c/h1\u003e\n\u003cp\u003eHomebrew是一个免费的、开源的软件包管理器，简化了在macOS上安装和管理软件的过程。使用Homebrew，您可以轻松地安装、更新和删除在macOS应用商店中不可用的软件包。由于其简单易用，它被开发人员和高级用户广泛采用。\u003c/p\u003e","title":"Homebrew初学者指南：macOS的软件包管理器"},{"content":"","permalink":"https://hobbyworker.me/zh-hans/projects/","summary":"","title":"项目"}]