3.5 自动打包工具

手写Manifest文件极其低效且容易出错,而Cocos2d-x官方又没有给出对应的工具,所以需要实现一个自动对比差异并打包的小工具来代替手动编辑Manifest文件。

我们希望用新版本的资源覆盖了旧版本的资源之后,通过工具来自动对比版本差异,为差异的资源自动生成Manifest文件。通过不同的打包方式,可以有两种增量更新的方法,这两种方法各有利弊,这里简单分析一下。

第一种是逐文件更新的方法,把要更新的资源放到服务器上,客户端每次更新时对比出远程Manifest文件和本地Manifest文件的差异资源,然后逐个下载。这种方式的好处是,当隔了很久没有去更新时,不论一个文件被修改了多少次,都只更新一次该文件,并且支持在新版本中删除资源文件。而缺点也很明显,如果文件很多,那么就需要下载很多次,一次只能下载一个文件,并且文件没有经过压缩,既消耗流量又消耗服务器资源,而且所有的资源都可以被访问(这也降低了资源的安全性,增加了资源被盗用的风险)。如果修改以图片为主(这里说的是修改而不是新增),那么可以用这种方式,因为图片本身的zip压缩率就很低。

假设游戏引用了名字为1.png~100.png的100张png图片,首次更新需要更新88.png,那么就把88.png放到服务器的资源目录下,生成只记录88.png的Manifest文件。第二次又更新了1.png~50.png,那么将这50张图片放到服务器的资源目录下,生成记录88.png以及1.png~50.png的Manifest文件。服务器的资源目录会一直保持与客户端本地的更新目录一一对应的关系。如果第三次更新删除了88.png,那么客户端更新到该版本之后本地更新目录的88.png也会被删掉,但安装包中最老版本的88.png是不会被删除的。

第二种是打包更新的方法,把本次要更新的资源打包然后放到服务器上,每个小版本都是一个更新包。每次更新时服务器的Manifest文件都会新增这个更新包到资源列表中,如果我们发布了10个版本,那么Manifest文件中会记录这10个版本的更新包。这种方式的好处是每次更新只需要更新一个文件即可,相对来说会更省流量,服务器的压力也会更小,但缺点是如果有很多较大的文件是频繁修改的,如巨大的公共图集,每次都修改了这个图集,那么10个zip包中就会有10张图集,如果隔了很久没有去更新时,就需要将中间每一个版本的更新包都下载下来,而且服务器的资源目录和客户端本地的更新目录不是一一对应的,无法实现删除某资源的操作。

对于打包更新的方法,可以通过优化打包的方式来弥补它的缺点,在每次打包时,遍历之前打好的包,将重复的资源剔除,如果之前的包中只剩下要剔除的资源,则删除这个包,并更新Manifest文件。这样既可以保证资源都被压缩,也可以避免当用户长期不登录时,再次登录时需要下载大量的冗余重复资源。

1.设计自动打包工具

接下来实现这个自动打包的小工具,用这个小工具来自动计算出每次版本更新的差异资源,并生成Manifest文件,而不是手动整理出差异资源并手写Manifest文件。打包工具的需求如下。

每次要发布新版本的时候,执行一次打包工具,指定资源路径以及版本号,打包工具自动遍历所有资源,对比与上一个版本资源的差异。可以选择将差异资源复制到一个资源发布目录下,或将差异资源打包压缩再移动到资源发布目录下,并生成Manifest文件。

在运行打包工具之前,先将资源放到打包工具下的资源目录下,覆盖旧的资源。首先需要用一个资源列表文件来记录最后一次更新时所有文件的状态,拿到最新版本的资源列表release.assets,如果这是第一个版本,那么直接生成这个版本的资源列表即可,后面所有的变化都是在这个基础版本的资源上进行变化的。可以遍历整个资源目录,将每一个资源的路径及其MD5码记录到资源列表中。

如果获取到了最新(上一个)版本的资源列表,则遍历整个资源目录,生成新版本的资源列表,对比两个资源列表,将新增以及MD5值不同的资源整理出来,打包成一个zip包,然后将新版本的资源列表保存为release.assets,替换为最新版本的资源列表,最后生成最新版本的Manifest文件。

也可以选择不生成zip包,实际上不生成zip包就是把最新的资源目录整个发布出去,然后将完整的资源列表记录到Manifest文件中。

我们需要解决以下几个问题。

❑ 如何遍历目录下的所有文件。

❑ 如何读写文件。

❑ 如何获取文件的MD5。

❑ 如何使用JSON编码和解码。

❑ 如何使用zip压缩文件。

这样的一个小工具用PHP或Python可以很方便地实现,这里使用PHP来实现。虽然PHP是专门用于实现Web服务器的脚本语言,但也可以用它来实现一些命令行小工具,在命令行中执行PHP解释器,传入要执行的PHP脚本即可在命令行中执行PHP, quick-cocos2d-x的很多命令行工具都是这么实现的。

2.使用打包工具

接下来了解一下如何使用打包工具进行打包,首先需要从控制台进入打包工具的目录,直接运行脚本不输入任何参数会弹出介绍说明,如图3-4所示。如果在Mac下的帮助说明是乱码,只需要将lib/pack_assets.php的文件编码转为UTF-8即可。

图3-4 打包工具

下面先执行一条命令来生成基础1.0.0版本。在Windows下执行packassets.bat res res/url http://localhost/test/ m zip命令,Mac下将packassets.bat修改为./packassets.sh,如图3-5所示,这条指令会将当前的res目录下的资源进行打包,传入指定的URL以及打包模式。由于是基础版本,所以不会生成增量更新包,只是记录了所有文件的状态。

图3-5 生成基础版本

然后在res目录中随意添加新文件,并对旧的文件进行修改,再执行一次刚才输入的命令,如图3-6所示,打包工具会自动对比出差异的文件并进行打包。打包的新版本号会自动在旧版本号的最后一位自增一,也可以通过version参数指定版本。默认会将生成的包生成到当前目录下的release目录中,也可以通过release参数指定发布路径。使用m参数可以指定zip打包和file逐文件打包两种模式。打包参数的作用在PHP脚本中有详细的介绍,这里不再细述。

图3-6 生成增量包

执行完命令之后会在指定的release目录下输出project.manifest、version.manifest、对应的版本资源以及记录所有资源详情的release.assets文件,如图3-7所示。将release目录下的文件复制到服务器的下载路径下(如在nginx的html目录下,根据指定的URL相对路径test目录中),即可发布新版本。

图3-7 release发布目录

3.自动打包工具的实现

接下来简单介绍一下这个小工具是如何实现的。首先需要将Windows和Mac下的PHP程序放到对应的目录下,然后编写一个bat和一个shell脚本,这种方式是参考quick-cocos2d-x的命令行工具实现,感兴趣的读者可以下载quick,然后打开quick下的bin目录学习一下。如不关心打包工具的实现,可以跳过这一节。

pack_assets.bat脚本的代码如下。

    @echo off
    set DIR=%~dp0
    %DIR%win32\php.exe "%DIR%lib\pack_assets.php" %*

pack_assets.sh脚本的代码如下。

    #!/bin/bash
    DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
    php "$DIR/lib/pack_assets.php" $*

然后在当前目录的lib目录下新建pack_assets.php文件,输入以下代码。

    <?php
    require_once 'jsbeautifier.php';
    define('DS', DIRECTORY_SEPARATOR);
    /* 自动打包脚本
      1.对比差异
      2.自动打包
    输入参数:
        res: 要打包的资源路径
        url: 下载链接
        release: 发布的路径
        asset: 默认为发布路径下的version.manifest
        version: 版本号[默认为1.0.0 或最后一个版本自增0.0.1]
        2016-7-5 by宝爷
    */

    function help()
    {
        echo <<<EOT
        本工具可用于Cocos2d-x增量更新自动打包
        生成增量更新包之前需要生成基础版本资源列表(用于对比差异)
        有两种情况会生成基础版本资源列表:
          1.首次运行或找不到最新资源列表时会自动生成
          2.输入新的大版本号时,如2.0.0
        之后修改资源目录,再次运行本工具即可生成更新资源包
        如果是file模式,生成了新的version和project文件之后,只需要将所有资源放到发布
        目录下即可
        大版本更新需要先清空release目录,此操作并不频繁,且需要慎重,所以本工具不进行自
        动处理

        输入参数:
        res: 要打包的资源路径
        release: 发布的路径[默认为当前目录下的release目录]
        version: 版本号[默认为1.0.0或最后一个版本自增0.0.1]
        m: 打包模式[默认为zip模式打包zip,可选file模式不打包]
        url: 下载链接[用于写入version和project Manifest文件中]

        注意,上述的路径均为当前目录的相对路径

        Example:
          pack_assets res ./res/ release../mygame/release/ m zip url http://localhost/
          test/ version 1.1.0
    EOT;
    }

    $options = array();
    #检查输入参数
    function checkArgs()
    {
        global $argc, $argv, $options ;
        $argsCheck = array(
          "res", "release", "m", "version", "url"
        );
        $argcCheck = count($argsCheck);
        # 获取输入的参数
        for($idx = 1; $idx < $argc; $idx++)
        {
          if($idx + 1 < $argc)
          {
              for($i = 0; $i < $argcCheck; ++$i)
              {
                  if($argv[$idx] == $argsCheck[$i])
                  {
                      $options[$argv[$idx] ] = $argv[++$idx];
                      print($options[$argv[$idx] ]);
                      break;
                  }
              }
          }
        }

        # 设置默认参数
        if(! array_key_exists("res", $options) || ! array_key_exists("url",
        $options))
        {
          help();
          return false;
        }
        if(! array_key_exists("release", $options))
        {
          $options["release"] = $options["res"] . "/../release/";
        }
        if(! array_key_exists("m", $options))
        {
          $options["m"] = "zip";
        }
        return true;
    }

    # 获取最后一次发布的所有文件状态
    function getLastRelease(array & $lastRelease)
    {
        global $options ;
        if(file_exists($options["release"] . "release.assets"))
        {
          $jsonFile      =      file_get_contents($options["release"]      .
    "release.assets");
          $lastRelease = json_decode($jsonFile, true);
          return true;
        }
        return false;
    }

    # 获取版本,输入的版本应该以a.b.c的格式
    function getVersion($lastVersion)
    {
        global $options ;
        if(array_key_exists("version", $options))
        {
          return $options["version"];
        }

        if($lastVersion ! = null)
        {
          $verArray = explode('.', $lastVersion);
          if(count($verArray) > 0)
          {
              $verArray[count($verArray) -1] += 1;
              return implode('.', $verArray);
          }
        }

        return "1.0.0";
    }

    # 检查一个版本字符串是不是一个大版本,即类似3.0.0这样的
    function isLargeVersion($version)
    {
        if($version == null) return false;
        $verArray = explode('.', $version);
        $arrayCount = count($verArray);
        if($arrayCount > 0)
        {
          for($idx = 1; $idx < $arrayCount; ++$idx)
          {
              if($verArray[$idx] ! = 0) return false;
          }
        }
        return true;
    }

    # 遍历目录,找出指定目录下的所有文件
    function findFiles($dir, array & $files)
    {
        $dir = rtrim($dir, "/\\");
        $dh = opendir($dir);
        if ($dh == false) {
          print("\nopen dir error\n");
          return;
        }

        while (($file = readdir($dh)) ! == false)
        {
          if ($file == '.' || $file == '..') { continue; }

          $path = $dir . '/' . $file;
          if (is_dir($path))
          {
              findFiles($path, $files);
          }
          elseif (is_file($path))
          {
              $files[] = $path;
          }
          else
          {
              print("error find " . $path);
          }
        }
        closedir($dh);
    }

    # 生成一个文件数组对应的MD5数组
    function genMD5(array & $files)
    {
        $filemd5s = array();
        $length = count($files);
        for($idx = 0; $idx < $length; $idx++)
        {
          $filemd5s[$files[$idx]] = md5_file($files[$idx]);
        }
        return $filemd5s;
    }

    # 生成一个新的Manifest数组
    function genVersionManifest($version, $url)
    {
        #$url = http://localhost/;
        $manifest = array(
          "packageUrl"=> $url,
          "remoteManifestUrl" => $url . "project.manifest",
          "remoteVersionUrl" =>$url . "version.manifest",
          "version" => $version
        );
        return $manifest;
    }

    # 将当前版本的更新追加到VersionManifest文件中
    function appendVersionManifest(array & $manifest, $subversion)
    {
        $manifest["version"] = $subversion;
        if(array_key_exists("groupVersions", $manifest))
        {
          $count = count($manifest["groupVersions"]) + 1;
          $manifest["groupVersions"][(string)$count] = $subversion;
        }
        else
        {
          $manifest["groupVersions"] = array("1" => $subversion);
        }
    }

    # 将当前版本的更新追加到ProjectManifest文件中
    function  appendProjectManifest(array  &  $manifest,  $subversion,  $file,
    $searchPath)
    {
        if(! array_key_exists("assets", $manifest))
        {
          $manifest["assets"] = array();
          $manifest["assets"][$subversion] = array();
        }
        $manifest["assets"][$subversion]["path"] = "release" . $subversion .
    ".zip";
        $manifest["assets"][$subversion]["md5"] = md5_file($file);
        $manifest["assets"][$subversion]["compressed"] = true;
        #$manifest["assets"][$subversion]["group"] = $group;
        if($searchPath ! = null)
        {
            if(! array_key_exists("searchPaths", $manifest))
            {
              $manifest["searchPaths"] = array ($searchPath);
            }
            else
            {
              $manifest["searchPaths"][] = $searchPath;
            }
        }
    }

    # 将文件添加到ProjectManifest文件中
    function appendResToProjectManifest(array & $manifest, $file)
    {
        global $options ;
        # 去掉头部
        $path = str_replace($options["res"], "", $file);
        if(! array_key_exists("assets", $manifest))
        {
            $manifest["assets"] = array();
        }
        if(! array_key_exists($path, $manifest["assets"]))
        {
            $manifest["assets"][$path] = array();
        }
        $manifest["assets"][$path]["md5"] = md5_file($file);
    }

    # 将指定的文件压缩到指定的release目录下的release + 版本号.zip文件
    如release1.0.0.zip
    function genZip($zipfile, array & $files)
    {
        global $options ;
        $zip = new ZipArchive();
        echo "compress to " . $zipfile . "\n";
        if (! $zip->open($zipfile, ZIPARCHIVE::OVERWRITE | ZIPARCHIVE::
        CM_STORE))
        {
            return false;
        }

        foreach ($files as $path => $md5)
        {
            # 保留res下的相对路径
            $file = str_replace($options["res"], "", $path);
            echo "\ncompress file " . $file . "\n";
            $zip->addFile($path, $file);
        }
        $zip->close();
        return true;
    }

    #保存Json文件
    function saveJsonFile($filePath, $manifest)
    {
        #
        $options = new BeautifierOptions();
        $beautifier = new JSBeautifier($options);
        $file = fopen($filePath, "w");
        $jsonStr = $beautifier->beautify(json_encode($manifest));
        fwrite($file, str_replace("\\", "", $jsonStr));
        fclose($file);
    }

    # 自动生成包
    function releaseVersion()
    {
        global $options ;
        # 检查参数
        if(! checkArgs()) return false;

        # 生成更新包
        $lastRelease = array();
        if(getLastRelease($lastRelease) && ! isLargeVersion($options["version"]))
        {
            $versionManifest = json_decode(file_get_contents($options
            ["release"]. "version.manifest"), true);
            if($versionManifest == null)
            {
              echo "decode " . $options["release"] . "version.manifest" . "
              faile\n";
              return false;
            }
            $projectManifest = json_decode(file_get_contents($options
            ["release"] . "project.manifest"), true);
            if($projectManifest == null)
            {
              echo "decode " . $options["release"] . "project.manifest" .
              "faile\n";
              return false;
            }

            # 检查版本更新
            $files = array();
            findFiles($options["res"], $files);
            $files = genMD5($files);
            $diffFiles = array_diff_assoc($files, $lastRelease);
            print("diff files is: \n");
            print_r($diffFiles);

            # 没有差异,无须打包
            if(count($diffFiles) == 0) return true;
  
            # 获得新版本号
            $version = getVersion($versionManifest["version"]);
            echo "version is " . $version . "\n";

            # zip模式打包
            if($options["m"] == "zip")
            {
              # 生成压缩包
              $zippkg = $options["release"] . "release" . $version . ".zip";
              genZip($zippkg, $diffFiles);

              # 更新VersionManifest和ProjectManifest
              appendVersionManifest($versionManifest, $version);
              saveJsonFile($options["release"] . "version.manifest",
              $versionManifest);
              appendVersionManifest($projectManifest, $version);
              appendProjectManifest($projectManifest, $version, $zippkg,
              null);
              saveJsonFile($options["release"] . "project.manifest",
              $projectManifest);
          }
          # file模式打包
          elseif($options["m"] == "file")
          {
              # 创建新的VersionManifest和ProjectManifest
              $versionManifest = genVersionManifest($version, $options["url"]);
              saveJsonFile($options["release"] . "version.manifest",
              $versionManifest);
              # 把所有文件写入project.manifest
              foreach ($files as $path => $md5)
              {
                  appendResToProjectManifest($versionManifest, $path);
              }
              saveJsonFile($options["release"] . "project.manifest",
              $versionManifest);
          }

          # 保存最新的版本资源MD5信息
          saveJsonFile($options["release"] . "release.assets", $files);
        }
        # 生成基础版本(不发布)
        else
        {
          $files = array();
          findFiles($options["res"], $files);
          $files = genMD5($files);
          @mkdir($options["release"] , 0700);
          # 保存最新的版本资源MD5信息
          saveJsonFile($options["release"] . "release.assets", $files);

          $version = getVersion(null);
          echo "version is " . $version;
          # 生成VersionManifest和ProjectManifest
          $versionManifest = genVersionManifest($version, $options["url"]);
          saveJsonFile($options["release"] . "version.manifest",
          $versionManifest);
          saveJsonFile($options["release"] . "project.manifest",
          $versionManifest);
        }
    }

    releaseVersion();

    echo "\nbuild version success ! ! ! \n"
    ?>