告别手动部署:为.NET嵌入式开发打造一键式ADB自动化部署流程
背景与痛点
在嵌入式设备(如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 '^total\s+' } 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) 展开为 bin\Debug\net8.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)结合,实现自动化测试和发布。甚至可扩展为支持多设备同时部署,进一步提升效率。
希望本文能帮助你摆脱繁琐的部署困扰,将精力更多投入到代码逻辑和功能创新上。如果你在实践中有任何疑问或优化建议,欢迎在评论区留言讨论!
以上信息来自AI,使用时请根据实际情况进行修改
浙公网安备 33010602011771号