背景与痛点
在嵌入式设备(如Android Things、Linux开发板、IoT网关等)上开发.NET应用时,我们通常会遇到这样的循环:
- 在Visual Studio 2022中编写代码
- 编译生成可执行文件与依赖库
- 通过
adb push手动复制文件到设备 - 用
adb shell chmod +x赋予执行权限 - 用
adb shell运行程序,查看输出
这一连串操作不仅繁琐,而且每次调试都要重复,极大地消耗了开发者的精力和时间。尤其当项目包含多个依赖文件时,手动复制极易遗漏或出错。
本文将向你展示如何利用Visual Studio 2022的“后期生成事件” + PowerShell脚本 + ADB,实现编译后自动部署、增量同步、权限赋予、独立窗口运行的全自动化流程,让嵌入式开发的迭代速度提升一个量级。
方案概览
- 触发时机:VS 2022 项目生成事件(Build Events)中的“后期生成事件”(Post-build event)
- 核心脚本:PowerShell(
.ps1),负责处理文件同步、对比、权限与运行 - 通信工具:ADB(Android Debug Bridge),用于与开发板交互
- 运行方式:弹出独立CMD窗口,支持交互式程序
最终效果:每次编译成功后,自动将变更的文件推送至设备,并立即在独立窗口中启动程序,全程无需人工干预。
准备工作
1. 安装ADB并配置环境变量
- 下载Android Platform Tools(包含
adb.exe) - 将ADB所在目录添加到系统
PATH环境变量,或直接在脚本中使用绝对路径 - 确保设备已开启USB调试并通过
adb devices可见
2. Visual Studio 2022 项目准备
- 任意.NET项目(控制台、类库等均可)
- 确保项目生成输出包含可执行文件(若为类库,则主程序为同名的无后缀文件)
详细实施步骤
第一步:编写PowerShell部署脚本
在解决方案根目录或项目目录下,新建一个文本文件,命名为Deploy.ps1。将以下完整脚本粘贴进去。
param(
[string]$LocalSrcDir,
[string]$DeviceDstRoot = "/data/local/tmp",
[string]$DeviceSubDir = "",
[string]$TargetFileName = ""
)
# 清洗可能的多余引号
$LocalSrcDir = $LocalSrcDir.Trim('"')
$DeviceSubDir = $DeviceSubDir.Trim('"')
$TargetFileName = $TargetFileName.Trim('"')
$DevicePath = if ($DeviceSubDir) { "$DeviceDstRoot/$DeviceSubDir" } else { $DeviceDstRoot }
Write-Host "Deploying to $DevicePath ..."
# 1. 创建设备目标目录
adb shell mkdir -p "$DevicePath" 2>$null
# 2. 一次性获取远程所有文件的大小(用于增量对比)
$remoteLs = adb shell ls -l "$DevicePath" 2>$null
$remoteFileSizes = @{}
if ($LASTEXITCODE -eq 0 -and $remoteLs) {
$lines = $remoteLs -split "`r`n" | Where-Object { $_ -and $_ -notmatch '^totals+' }
foreach ($line in $lines) {
$fields = $line.Trim() -split 's+'
if ($fields.Count -ge 9) {
$size = [int64]$fields[4] # ls -l 的第5列是大小
$fname = $fields[-1] # 最后一列是文件名
$remoteFileSizes[$fname] = $size
}
}
}
# 3. 获取本地所有文件
$LocalFiles = Get-ChildItem -Path $LocalSrcDir -File
# 4. 遍历并仅推送大小变化的文件
foreach ($file in $LocalFiles) {
$localSize = $file.Length
$remoteSize = $remoteFileSizes[$file.Name]
if ($remoteSize -ne $null -and $localSize -eq $remoteSize) {
Write-Host "Skipping $($file.Name) (size unchanged)"
continue
}
Write-Host "Pushing $($file.Name) ..."
adb push "$($file.FullName)" "$DevicePath/$($file.Name)"
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to push $($file.Name)"
exit 1
}
}
# 5. 处理可执行文件:去除 .dll 后缀,赋权并弹出CMD运行
if ($TargetFileName) {
$ExeBaseName = $TargetFileName
if ($ExeBaseName -match '.dll$') {
$ExeBaseName = $ExeBaseName -replace '.dll$', ''
Write-Host "Detected .dll, using base name: $ExeBaseName"
}
$remoteExe = "$DevicePath/$ExeBaseName"
adb shell test -f "$remoteExe" 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Warning "Executable '$remoteExe' not found on device. Skipping run."
} else {
Write-Host "Setting execute permission for $remoteExe"
adb shell chmod +x "$remoteExe"
# 弹出独立的CMD窗口,支持交互
Write-Host "Launching CMD window for $remoteExe ..."
Start-Process cmd -ArgumentList "/k adb shell `"$remoteExe`""
# 若希望程序结束后窗口自动关闭,可将 /k 改为 /c
}
}
Write-Host "Deployment completed."
脚本核心设计思想
- 增量同步:通过
ls -l一次性获取远程文件大小,与本地文件大小比对,仅推送变更文件,大幅减少传输时间。 - 后缀处理:自动识别
.dll并剥离,适用于生成类库的项目(实际可执行文件为无后缀同名文件)。 - 独立窗口:使用
Start-Process cmd打开新窗口,避免程序在VS后台阻塞,且支持交互式输入(如串口调试、菜单操作)。 - 健壮性:检查文件存在性,错误时退出并返回非零码,让VS感知失败。
第二步:配置Visual Studio后期生成事件
- 在解决方案资源管理器中,右键点击你的项目 → 选择 “属性”。
- 切换到 “生成事件” 选项卡。
- 在 “后期生成事件命令行” 文本框中,输入以下命令:
powershell -ExecutionPolicy Bypass -File $(SolutionDir)Deploy.ps1 -LocalSrcDir "$(TargetDir)" -DeviceDstRoot "/root" -DeviceSubDir "$(ProjectName)" -TargetFileName "$(TargetFileName)"
参数说明:
| 参数 | 含义 | 示例 |
|---|---|---|
-LocalSrcDir |
本地生成输出目录 |
$(TargetDir) 展开为 binDebugnet8.0
|
-DeviceDstRoot |
设备目标根目录 | 可自定义,如 /sdcard/
|
-DeviceSubDir |
设备子目录名(建议用项目名隔离) | $(ProjectName) |
-TargetFileName |
主可执行文件名(含后缀) | $(TargetFileName) |
注意:
$(SolutionDir)和$(ProjectDir)是VS宏,会在构建时展开。确保脚本保存在解决方案根目录,或调整为实际路径。
第三步:编译测试
按 Ctrl+Shift+B 或点击“生成”按钮,观察“输出”窗口。你会看到类似以下的日志:
Deploying to /data/local/tmp/MyProject ... Pushing MyApp.dll ... Pushing MyApp.runtimeconfig.json ... Skipping SomeLib.dll (size unchanged) Detected .dll, using base name: MyApp Setting execute permission for /data/local/tmp/MyProject/MyApp Launching CMD window for /data/local/tmp/MyProject/MyApp ... Deployment completed.
同时,一个新CMD窗口会弹出,其中运行你的设备程序,你可以进行交互操作。
进阶定制
1. 修改运行参数
若你的程序需要命令行参数,可修改脚本中的运行行:
Start-Process cmd -ArgumentList "/k adb shell `"$remoteExe --port 8080 --verbose`""
2. 后台运行(不弹出窗口)
若希望静默运行(不显示窗口),可使用:
Start-Process -WindowStyle Hidden -FilePath "adb" -ArgumentList "shell $remoteExe"
但此时无法交互,适合服务类程序。
3. 清理远程多余文件
如果需要在推送前删除设备目录下本地已不存在的文件,可在获取远程文件后增加对比逻辑,使用 adb shell rm 删除。
4. 支持子目录结构
若你的输出包含子文件夹(如 publish 目录),可将 Get-ChildItem -File 改为 Get-ChildItem -Recurse -File,并调整远程路径的拼接以保留相对路径。
常见问题与解决办法
Q1: 执行脚本时提示“无法加载,因为在此系统上禁止运行脚本”
原因:PowerShell执行策略限制。
解决:在调用命令中添加 -ExecutionPolicy Bypass,我们已经加上。若仍失败,可临时修改系统策略(以管理员身份运行 Set-ExecutionPolicy RemoteSigned)。
Q2: 后期生成事件中参数被错误解析,导致路径包含额外字符串
原因:引号使用不当或空格导致。
解决:确保每个参数值都用双引号括起,且参数名与值之间有空格。避免将整个参数字符串放在一个引号内。
Q3: ls -l 输出格式与脚本预期不符(如列数不同)
原因:不同嵌入式系统(如Android、OpenWrt)的 ls 输出可能略有差异(如缺少用户组列)。
解决:在设备上手动执行 adb shell ls -l /data/local/tmp/,观察字段位置。若大小字段不在第5列(索引4),调整脚本中 $fields[4] 的索引值。
Q4: 设备端程序运行后立即退出,看不到输出
原因:程序执行完即结束,窗口可能闪退。
解决:若你使用 /k 参数,窗口不会关闭,可以查看输出。若程序本身是快速退出,可在程序末尾增加 Console.ReadKey() 或延时。
Q5: 权限不足,无法给文件添加执行权限
原因:ADB shell 可能不是 root 用户。
解决:若文件在 /data/local/tmp 下,通常普通用户也可 chmod。如果仍失败,尝试使用 adb root 获取 root 权限(需设备支持)。
总结与延伸
通过上述方案,我们成功将“复制-赋权-运行”三部曲整合到一次编译过程中,不仅节省了大量手动操作时间,还通过文件大小比对实现了智能增量同步。这套流程特别适合嵌入式开发的快速迭代场景,无论是C#、.NET Core还是.NET 5/6/7/8项目,均可适用。
未来,你还可以将这套脚本与CI/CD系统(如GitHub Actions、Azure DevOps)结合,实现自动化测试和发布。甚至可扩展为支持多设备同时部署,进一步提升效率。
希望本文能帮助你摆脱繁琐的部署困扰,将精力更多投入到代码逻辑和功能创新上。如果你在实践中有任何疑问或优化建议,欢迎在评论区留言讨论!
到此这篇关于告别手动部署:为.NET嵌入式开发打造一键式ADB自动化部署流程的文章就介绍到这了,更多相关.NET嵌入式开发ADB自动化部署内容请搜索IT俱乐部以前的文章或继续浏览下面的相关文章希望大家以后多多支持IT俱乐部!
