不会用ant打包、部署项目的工程师,不是一个好程序员(测试)

简介:

副标题:利用ant脚本 自动构建svn增量/全量 系统程序升级包

首先请允许我这样说,作为开发或测试,你一定要具备这种本领。你可以手动打包、部署你的工程,但这不是最好的方法。最好的方式就是全自动化的方式。开发人员提交了代码后,可以自动构建、打包、部署到测试环境。测试通过后进入到模拟环境或是直接发布的生产环境,这个过程可以是全自动的。但这个自动化的方式有一些公司用到了,但也有很多公司还不知道,他们的攻城师天天在做反复、没有多大意义的、浪费生命的事情。当然这种方式一般针对做自己的产品,如电商、通信行业。而给其他公司或企业做项目的比较少利用这种方式,当然也可以利用这种方式,只是不能直接发布到客户的服务器而已。当然有些公司是专门有人做这部分事情的!

说了这么多也没有什么恶意,只是觉得这种方式可以大大提高效率,降低人力、物力、财力而已。勿喷,O(∩_∩)O哈哈~嘿嘿~

在部署项目或打包项目中,通常大家都是手动部署或打包的多。很多公司把这一任务交个了我们做开发的,其实这部分应该谁做呢?本质上应该是测试的来完成,但一些公司的测试不会做这个,慢慢的就变成了开发的事情。有些公司是人手比较少、不健全,所以一部分人自己承担了这个事情。在我们手动打包的时,其实这是一个重复的、没有技术含量的、耗费体力的活儿。一般步骤就是更新svn上代码、修改好相关的配置、编译class、发布到tomcat(web工程)、测试启动无误、手动打包。

而有一种比较简单快速的方式就是利用meven或ant来完成这些工作,只要我们编写好脚本后。给相关的工作人员去运行这部分脚本就可以完成打包,甚至是部署项目,这些都是so easy~!我个人也比较赞成使用这种方式,比较简单、快速、重用性好,最难的可能编写build脚本,但这个脚本其实也是很简单的,它就想dos命令行,只不过它是用xml方式来完成命令行的而已。所以测试会写ant的build脚本这个也是应该的,如果你不会的话,还是建议你学习学习。反正是百益无一害的事情,何乐而不为呢!

 

一、基本流程

利用ant打包项目或打增量包的基本流程

image

值得一提的是jar包这个部分,这个步骤是为下面编译增量包做准备的。因为增量包导出的增量文件,它依赖于整个项目的其他代码,如果没有这些代码的支持是编译不通过。然而又不能直接通过diff得到增量的class,所以只能导出增量文件后,通过引用全部工程的代码的class再进行编译即可。

 

二、运行环境

1、安装jdk,不会自己上网查其他的

2、如果你还没有安装ant,那么你可以参考:http://www.cnblogs.com/hoojo/archive/2013/06/14/java_ant_project_target_task_run.html

会介绍一下ant的安装和使用的方法。

3、这里需要用到svn的ant相关工具包、命令支持。你需要下载svnant-1.3.1.zip,将里面的lib库放置在你的ant脚本的编译运行环境中。

4、因为某些项目使用到了泛型、annotation注解,使用javac有些代码是编译不通过的,所以这里使用了jdt的编译方式。参考:使用eclipse JDT compile class 会有很详细的介绍。

需要用到

jdtCompilerAdapter.jar 
org.eclipse.jdt.compiler.tool_1.0.1.v_793_R33x.jar 
org.eclipse.jdt.core_3.3.3.v_793_R33x.jar 
org.eclipse.jdt.debug.ui_3.2.102.v20071002_r332.jar 
复制到ant_home/lib目录下,如果是利用eclipse运行脚本就需要把它加载到运行环境中。可以参考上面的:使用eclipse JDT compile class

 

三、编写ant的build脚本

1、首先看看ant工程的目录结构

image

简单介绍下目录结构:

src下面的ExportIncrementFiles.java是导出增量文件要用的,它在build命令increment中执行。它会读取diff 比较后的文件中的内容,并导出文件

dest 是checkout出来最新的svn的工程

dist 是编译上面dest目录中的工程,也是svn全量war的工程目录和jar

increment_dest 是增量工程,也就是上面的ExportIncrementFiles工具导出的工程

increment_dist 是编译上面increment_dest 的工程,也是增量包的工程目录

因为每个人的项目工程目录结构不一样,所以这个脚本并不会通用,我这里指针对自己的项目进行测试。

lib中是运行环境需要的jar库,其中主要的就是svnlib 这个你可以去下载 svnant-1.3.1.zip 以及JDT编译class的jar包,这个可以通过eclipse中的plugin中的jar包找到,可以参考:使用eclipse JDT compile class

increment.export.jar就是ExportIncrementFiles的class打成的jar包,这个是自己打的包,可以直接应用class也可以的,在increment命令引用,jar下载:http://download.csdn.net/detail/ibm_hoojo/6501165

build.properties是当前build的配置文件

build.xml就是主要的ant脚本

选中部分是打的war包,这个就可以部署了

patch.txt就是svn的diff 比较出的增量文件的目录路径列表

 

2、ExportIncrementFiles.java 导出增量文件

package com.hoo.util;
 
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
 
/**
 * <b>function:</b> 导出在增量的文件
 * @author hoojo
 * @createDate 2013-11-2 下午10:00:01
 * @file ExportIncrementFiles.java
 * @package com.hoo.util
 * @project AntTest
 * @blog http://blog.csdn.net/IBM_hoojo
 * @email hoojo_@126.com
 * @version 1.0
 */
public class ExportIncrementFiles {
 
    /**
     * <b>function:</b> 导出增量文件
     * @author hoojo
     * @createDate 2013-11-2 下午10:15:43
     * @param configPath 增量文件路径配置目录
     * @param baseDir 基本路径 目标位置
     * @param destDir 增量文件保存位置
     * @throws Exception
     */
    private static void export(String configPath, String baseDir, String destDir) throws Exception {
        String srcFile = baseDir + configPath;
        String desFile = destDir + configPath;
        
        int lastIndex = desFile.lastIndexOf("/");
        String desPath = desFile.substring(0, lastIndex);
        
        File srcF = new File(srcFile);
        if(srcF.exists()){//如果不存在这样的源文件,就不再拷贝,这个用来解决版本之间有删除文件的情况。
            File desF = new File(desFile);
            File desP = new File(desPath);
            if(!desP.exists()) {
                desP.mkdirs();
            }
            System.out.println(srcFile);
            FileInputStream fis = new FileInputStream(srcF);
            FileOutputStream fos = new FileOutputStream(desF);
            
            byte[] buf = new byte[1024];
            int len = 0;
            while((len = fis.read(buf)) != -1) {
                fos.write(buf,0,len);
            }
            fos.flush();
            fos.close();
            fis.close();
        }
    }
    
    /**
     * <b>function:</b> 主函数 执行导出增量包任务
     * @author hoojo
     * @createDate 2013-11-2 下午10:00:01
     * @param args 参数1 增量包导出文件路径,参数2 要导出的文件的所在目标位置,参数3 增量包导出保存的位置路径
     */
    public static void main(String[] args) {
 
        if (args.length > 0) {
            if (args.length == 1 && "help".equals(args[0])) {
                System.out.println("args[0] is Export Increment Files content path");
                System.out.println("args[1] is Export Increment Files target path");
                System.out.println("args[2] is Increment Files Export loaction");
            } else {
                String configPath = args[0];
                String baseDir = args[1];
                String destDir = args[2];
                
                try {
                    BufferedReader br = new BufferedReader(new FileReader(configPath));
                    String s = null;
                    while((s = br.readLine()) != null) {
                        s = s.trim();//去掉路径前面的空格
                        String str = destDir + s;
                        if(!destDir.equals(str)){//过滤空行
                            export(s, baseDir, destDir);
                        }
                    }
                    br.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

main函数参数看注释,主要就是读取patch.txt增量文件路径,导出文件到指定目录的。

 

3、build 脚本

<?xml version="1.0" encoding="UTF-8" ?>
<!-- createDate 2013-10-28 -->
<!-- author hoojo & http://blog.csdn.net/IBM_hoojo & http://hoojo.cnblogs.com -->
<project default="checkout" basedir=".">
    <property file="build.properties"/>
    
    <!-- svn 比较项目最新路径 -->
    <property name="svn.url" value="${svn._url}"/>
    <!-- svn 备份路径-->
    <property name="bak.svn.url" value="${bak.svn._url}"/>
    
    <property name="svn.username" value="${svn.username}"/>
    <property name="svn.password" value="${svn.password}"/>
    
    <!-- 项目名称 -->
    <property name="webapp" value="${webapp.name}"/>
    <!-- 目标项目的Web 名称(WEB-INF上一级的目录名称) -->
    <property name="webroot" value="${web.root}"/>
    
    <!-- svn改动文件列表信息 -->
    <property name="compare.path.file" value="${increment.file}"/>
    
    <!-- svn导出/切出文件存放目录 -->
    <property name="dest.path" location="dest/${webapp}"/>
    <!-- svn导出/切出文件编译后存放目录 -->
    <property name="dist.path" location="dist/${webapp}"/>
    <!-- svn增量文件保存目录 -->
    <property name="increment.dest.path" location="increment_dest/${webapp}"/>
    <!-- svn增量文件编译后保存目录 -->
    <property name="increment.dist.path" location="increment_dist/${webapp}"/>
    
    <!-- 利用jdt编译class 解决泛型不能转换的问题 需要将
        jdtCompilerAdapter.jar
        org.eclipse.jdt.compiler.tool_1.0.1.v_793_R33x.jar
        org.eclipse.jdt.core_3.3.3.v_793_R33x.jar
        org.eclipse.jdt.debug.ui_3.2.102.v20071002_r332.jar
        复制到ant_home/lib目录下
    <property name="build.compiler" value="org.eclipse.jdt.core.JDTCompilerAdapter"/>
    -->
    <path id="svnant.classpath">
        <fileset dir="${basedir}">
            <include name="**/*.jar"/>
        </fileset>
    </path>
    
    <path id="buildpath">
        <fileset dir="${dest.path}">
            <include name="**/lib/*.jar"/>
        </fileset>
        <fileset dir="C:/Program Files/Java/jdk1.6.0_13">
            <include name="**/*.jar"/>
        </fileset>
    </path>
    
    <typedef resource="org/tigris/subversion/svnant/svnantlib.xml" classpathref="svnant.classpath"/>
    <svnSetting id="svn.settings" javahl="false" svnkit="true" username="${svn.username}" password="${svn.password}" failonerror="true"/>
    
    <target name="init" description="init clean dirs">
        <echo message="${svn.username}"/>
        <echo message="${svn.password}"/>
        <echo message="${webapp}"/>
        <echo message="${webroot}"/>
        <echo message="${compare.path.file}"/>
        
        <delete dir="${dest.path}" failonerror="false" deleteonexit="true" excludes="**/lib"/>
        <delete dir="${dist.path}" failonerror="false" deleteonexit="true" excludes="**/lib"/>
        <delete file="${compare.path.file}" failonerror="false"/>
        <delete dir="${increment.dest.path}" failonerror="false" deleteonexit="true"/>
        <delete dir="${increment.dist.path}" failonerror="false" deleteonexit="true"/>
    </target>
    
    <!-- that is to test i svnant is available //-->
    <target name="tool-available" depends="init">
        <echo message="run task test svnant is available"></echo>
        <available resource="org/tigris/subversion/svnant/svnantlib.xml" classpathref="svnant.classpath" property="available.svnant"/>
        <echo message="SVN-ANT is available = ${available.svnant}"></echo>
    </target>
    
    <!-- 比较差异 增量文件 -->
    <target name="diff" description="deff/compare project">
         <svn refid="svn.settings">
            <diffSummarize oldUrl="${bak.svn.url}" newUrl="${svn.url}" outFile="${compare.path.file}" recurse="true"/>
         </svn>
    </target>
        
    <!-- 下载 切成 导出 服务器上最新代码 -->
    <target name="checkout" depends="tool-available" description="checkout/export project code ${svn.url} ">
        <echo message="checkout/export project code ${svn.url}"></echo>
        <svn refid="svn.settings">
             <export srcUrl="${svn.url}" destPath="${dest.path}" revision="HEAD" force="true"/>
        </svn>
   </target>
    
    <!-- javac编译 -->
    <target name="compile">
        <buildnumber/>
        <echo>compile ${dest.path} ......</echo>
        <delete dir="${dist.path}" failonerror="false" deleteonexit="true" excludes="**/lib"/>
        <mkdir dir="${dist.path}/classes"/>
        
        <javac nowarn="true" debug="${javac.debug}" debuglevel="${javac.debuglevel}" destdir="${dist.path}/classes" source="${javac.source}" target="${javac.target}" encoding="utf-8" fork="true" memoryMaximumSize="512m" includeantruntime="false">
            <src path="${dest.path}/src"/>
            <!--
            <compilerarg value="-Xlint:unchecked"/>
            <compilerarg value="-Xlint:deprecation"/>
            <compilerarg value="-Xlint"/>
            -->
            <classpath refid="buildpath"/>
            <classpath refid="svnant.classpath"/>
        </javac>
    </target>
    
    <!-- 利用JDT编译 -->
    <target name="compile_jdt">
        <buildnumber/>
        <echo>compile ${dest_path} ......</echo>
        <delete dir="${dist_path}" failonerror="false" deleteonexit="true" excludes="**/lib"/>
        <mkdir dir="${dist_path}/classes"/>
        
        <javac compiler="org.eclipse.jdt.core.JDTCompilerAdapter" nowarn="true" debug="${javac.debug}" debuglevel="${javac.debuglevel}" destdir="${dist_path}/classes" source="${javac.source}" target="${javac.target}" encoding="utf-8" fork="true" memoryMaximumSize="512m" includeantruntime="false">
            <src path="${dest_path}/src"/>
            
            <classpath refid="buildpath"/>
            <classpath refid="svnant.classpath"/>
        </javac>
    </target>
    
    <!-- 利用JDT编译SVN 最新项目 -->
    <target name="compile_svn">
        <!-- 回调任务 -->
        <antcall target="compile_jdt">
            <param name="dest_path" value="${dest.path}"/>
            <param name="dist_path" value="${dist.path}"/>
        </antcall>
    </target>
    
    <!-- 将全部项目的class 建立jar包 -->
    <target name="jar" depends="compile_svn">
        <jar destfile="${basedir}/lib/${webapp}.jar" level="9" compress="true" encoding="utf-8" basedir="${dist.path}/classes">
            <manifest>
                <attribute name="Implementation-Version" value="Version: 2.2"/>
            </manifest>
        </jar>
    </target>
    
    <!-- 导出增量文件 -->
    <target name="increment" depends="diff">
        <java classname="com.hoo.util.ExportIncrementFiles" classpath="${basedir}/lib/increment.export.jar" fork="true">
            <arg value="${compare.path.file}"/>
            <arg value="${dest.path}/"/>
            <arg value="${increment.dest.path}/"/>
        </java>
    </target>
 
    <!-- 利用JDT编译增量文件 -->
    <target name="compile_increment">
        <antcall target="compile_jdt">
            <param name="dest_path" value="${increment.dest.path}"/>
            <param name="dist_path" value="${increment.dist.path}"/>
        </antcall>
    </target>
    
    <!-- 全部打包 -->    
    <target name="war">
        <echo>create war file.......</echo>
        
        <copy todir="${dist_path}" failonerror="false">
            <fileset dir="${dest_path}/${webroot}" includes="**"/>
        </copy>
        <move todir="${dist_path}/WEB-INF/classes" failonerror="false">
            <fileset dir="${dist_path}/classes" />
        </move>
        <copy todir="${dist_path}/WEB-INF/classes" failonerror="false">
            <fileset dir="${dest_path}/src/main/" includes="**/*.xml, **/*.properties, **/*.xsd"/>
            <fileset dir="${dest_path}/src/test/" includes="**/*.xml, **/*.properties, **/*.xsd"/>
            <fileset dir="${dest_path}/src/resource/" includes="**/*.xml, **/*.properties, **/*.xsd"/>
        </copy>
        
        <!--得到当前日期--> 
        <tstamp> 
            <format property="DSTAMP" pattern="yyyyMMdd" locale="zh"/> 
            <format property="TSTAMP" pattern="HHmmss" locale="zh"/> 
        </tstamp> 
        
        <war destfile="${basedir}/${webapp}_${DSTAMP}_${TSTAMP}.war" basedir="${dist_path}" webxml="${dist_path}/WEB-INF/web.xml"/>        
    </target>
    
    <!-- 全部打包 -->    
    <target name="war_svn">
        <antcall target="war">
            <param name="dest_path" value="${dest.path}"/>
            <param name="dist_path" value="${dist.path}"/>
        </antcall>        
    </target>
    
    <!-- 全部打包 -->    
    <target name="war_increment">
        <copy todir="${increment.dist.path}/WEB-INF" file="${dest.path}/${webroot}/WEB-INF/web.xml"/>
        <antcall target="war">
            <param name="dest_path" value="${increment.dest.path}"/>
            <param name="dist_path" value="${increment.dist.path}"/>
        </antcall>        
    </target>
    
    <!-- svn 全量包 -->
    <target name="svn_war" depends="checkout, compile_svn, war_svn"/>
    <!-- 增量包 -->
    <target name="increment_war" depends="checkout, increment, jar, compile_increment, war_increment"/>
</project>

 

4、build的配置文件内容

#Mon, 04 Nov 2013 11:18:12 +0800
svn._url=http://172.31.100.100/svn/iMVS_DataComm2
bak.svn._url=http://172.31.100.100/svn/iMVS_DataComm
svn.username=hoojo
svn.password=mypass
webapp.name=iMVS_DataComm
web.root=WebRoot
increment.file=patch.txt
 
javac.debuglevel=source,lines,vars
javac.target=1.6
javac.source=1.6
javac.debug=true

 

运行svn_war任务可以打全部的包,也就是svn最新地址的项目工程包。

运行increment_war任务可以打增量包,也会形成一个war文件。

如果你需要发布到tomcat目录,可以写一个任务copy相关war包到tomcat的webapps的目录下,这个很简单~如果你需要调用tomcat的相关任务或命令,你需要在build脚本中加入

<target name="_def_tomcat_tasks">
  <!-- tasks: deploy,undeploy,reload,stop,start,list,roles,resources -->
  <taskdef name="deploy"    classname="org.apache.catalina.ant.DeployTask" />
  <taskdef name="list"      classname="org.apache.catalina.ant.ListTask" />
  <taskdef name="reload"    classname="org.apache.catalina.ant.ReloadTask" />
  <taskdef name="resources" classname="org.apache.catalina.ant.ResourcesTask" />
  <taskdef name="roles"     classname="org.apache.catalina.ant.RolesTask" />
  <taskdef name="start"     classname="org.apache.catalina.ant.StartTask" />
  <taskdef name="stop"      classname="org.apache.catalina.ant.StopTask" />
  <taskdef name="undeploy"  classname="org.apache.catalina.ant.UndeployTask" />
</target>

关于这些命令使用方法有兴趣的可以自己研究研究。

 

四、总结

整个流程稍微有点复杂,只要思路清晰这个build脚本还是很容易编写的。前提是你要懂得Java发布、编译、部署的流程,很多人用eclipse或MyEclipse来发布工程,好像很简单。其实在工具背后也是使用这些脚本完成的,知道在用户目录下有一个 .m2的目录么,这个就是eclipse工具的meven的缓存和配置的内容。所以当我们不使用这些ide的情况下,你怎么编译、部署你的项目呢~!这篇文章只是一个抛砖引玉的效果,希望能给大家一个启示。在这之前,我在网上搜集了些资料也没有找到打增量包的比较好的方法,全都是手动方式。

同时,在一些自动集成或持续集成的智能工具中,也大量的使用到了这方面的技术。如果你想更智能的完成这个项目的发布、部署的话,这里只是其中的第一步。有兴趣的朋友可以研究下Continuous integration或Hudson等相关自动化集成技术应用。






本文转自hoojo博客园博客,原文链接:http://www.cnblogs.com/hoojo/p/ant_increment_svn_diff_diffSummarize.html,如需转载请自行联系原作者
目录
相关文章
|
8月前
|
人工智能 测试技术 项目管理
测试不再碎片化:AI智能体平台「项目资料套件」功能上线!
在实际项目中,需求文档分散、整理费时、测试遗漏等问题常困扰测试工作。霍格沃兹推出AI智能体测试平台全新功能——项目资料套件,可将多个关联文档打包管理,并一键生成测试用例,提升测试完整性与效率。支持套件创建、文档关联、编辑删除及用例生成,适用于复杂项目、版本迭代等场景,助力实现智能化测试协作,让测试更高效、更专业。
|
7月前
|
人工智能 自然语言处理 测试技术
让AI帮你跑用例-重复执行,不该成为测试工程师的主旋律
测试不该止步于重复执行。测吧科技推出用例自动执行智能体,通过AI理解自然语言用例,动态规划路径、自主操作工具、自动重试并生成报告,让测试工程师从“点点点”中解放,专注质量思考与创新,提升效率3倍以上,节约人力超50%,重构测试生产力。
|
8月前
|
测试技术 UED 开发者
性能测试报告-用于项目的性能验证、性能调优、发现性能缺陷等应用场景
性能测试报告用于评估系统性能、稳定性和安全性,涵盖测试环境、方法、指标分析及缺陷优化建议,是保障软件质量与用户体验的关键文档。
|
9月前
|
算法 测试技术 API
从自学到实战:一位测试工程师的成长之路
在技术快速发展的今天,自动化测试已成为提升职场竞争力的关键技能。本文讲述了一位测试工程师从自学到实战的成长之路,分享他在学习UI、APP和API自动化过程中遇到的挑战,以及如何通过实际项目磨炼技术、突破瓶颈。他从最初自学的迷茫,到实战中发现问题、解决问题,再到得到导师指导,逐步掌握测试开发的核心思维,并向测试平台建设方向迈进。文章总结了他从理论到实践、从执行到思考的转变经验,强调了实战、导师指导和技术服务于业务的重要性。最后,邀请读者分享自己的技术突破故事,共同交流成长。
|
9月前
|
Prometheus 监控 Cloud Native
测试开发工程师的必备法宝:性能监控与分析工具全面指南
在软件开发高速迭代的今天,性能问题直接决定着产品质量和用户体验。作为测试开发工程师,熟练运用专业的性能监控与剖析工具,不仅能精准定位系统瓶颈,更能为优化决策提供可靠依据。本文将系统解析当前主流的性能监控与剖析工具及其应用场景。
|
10月前
|
Java 测试技术 Spring
简单学Spring Boot | 博客项目的测试
本内容介绍了基于Spring Boot的博客项目测试实践,重点在于通过测试驱动开发(TDD)优化服务层代码,提升代码质量和功能可靠性。案例详细展示了如何为PostService类编写测试用例、运行测试并根据反馈优化功能代码,包括两次优化过程。通过TDD流程,确保每项功能经过严格验证,增强代码可维护性与系统稳定性。
364 0
|
10月前
|
人工智能 Java 测试技术
Java or Python?测试开发工程师如何选择合适的编程语言?
测试工程师如何选择编程语言?Java 还是 Python?多位资深专家分享建议:Python 入门简单、开发效率高,适合新手及自动化测试;Java 生态成熟,适合大型项目和平台开发。建议结合公司技术栈、个人基础及发展方向选择。长远来看,两者兼通更佳,同时关注 Go 等新兴语言。快速学习与实践才是关键。
|
人工智能 自然语言处理 JavaScript
测试工程师要失业?Magnitude:开源AI Agent驱动的端到端测试框架,让Web测试更智能,自动完善测试用例!
Magnitude是一个基于视觉AI代理的开源端到端测试框架,通过自然语言构建测试用例,结合推理代理和视觉代理实现智能化的Web应用测试,支持本地运行和CI/CD集成。
2140 15
测试工程师要失业?Magnitude:开源AI Agent驱动的端到端测试框架,让Web测试更智能,自动完善测试用例!
|
10月前
|
人工智能 数据可视化 测试技术
UAT测试排程工具深度解析:让验收测试不再失控,项目稳稳上线
在系统交付节奏加快的背景下,“测试节奏混乱”已成为项目延期的主因之一。UAT测试排程工具应运而生,帮助团队结构化拆解任务、清晰分配责任、实时掌控进度,打通需求、测试、开发三方协作闭环,提升测试效率与质量。本文还盘点了2025年热门UAT工具,助力团队选型落地,告别靠表格和群聊推进测试的低效方式,实现有节奏、有章法的测试管理。
|
缓存 Java 测试技术
【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了
【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了
2032 3
【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了