绝版-打造composer离线仓库

Ace华

应用场景

本脚本适用于无互联网环境,若你的开发环境有互联网,可以退出了。
情景:之前通过互联网离线导入了一个项目A,已经安装好全部依赖了,比如 webman/admin依赖。但在离线环境下,我又想在新的项目B中也安装webman/admin,通过这个脚本,你可以轻松做到,脚本代码会在最后全部贴出。

本工具的作用就是将之前已经安装好依赖的composer项目中的依赖提取出来,放到指定目录下,供其他目录离线安装使用

注意,一定要有其他项目已经安装好依赖的,也就是说,目录下一定要有 composer.lock和vendor目录

思路

因为无互联网,无法使用composer require来安装需要的包,之前的项目下载的包在vendor目录下是没有version信息的,但版本信息保存在了 composer.lock 文件中。
所以,本脚本的目标就是,将 composer.lock文件中的版本信息提取出来,写入每个包的composer.json文件中,并且通过下面命令,将每一个包都压缩成zip文件。

composer archive --format=zip --dir=$dest

打成zip后,就可以搭配上在composer.json中配置离线仓库就可以使用这些zip的包。

一个包打成zip格式之前,如果json文件中没有版本信息的话,在使用离线安装的时候不能做到像有互联网那样递归安装依赖所依赖的其他依赖。

操作使用

总体就是先提取其他项目的依赖,到指定目录,新项目需要安装依赖时,可以直接使用composer require XX 来安装包,方便快捷

1.脚本源码

将以下代码复制保存到php文件,这里叫 generate_repositories.php

<?php

main(); // 执行main()方法

function main()
{
    // 因为仓库是离线的,composer.json 中没有带version信息,需要根据composer.lock中提取版本信息,写到每个包的composer.json中去,再执行 composer archive 操作
    // 将每个库打包成zip包放到zips目录下
    // 会将composer.lock目录下的vendor包全部拷贝到目标目录下再进行修改composer.json,源目录下的composer.json文件保持不变

    $lock_file = "D:/4.PHP/Code/wfdemo/composer.lock"; // 修改为已安装好依赖项目根目录下的composer.lock绝对路径,注意\要改为/
    $zip_dest_dir = "D:/4.PHP/Code/wfdemo_repos";// 修改为离线仓库目录,再次运行旧的不会清空
    run($lock_file, $zip_dest_dir);
}

// 读取文件内容
function read_file($file)
{
    return !is_file($file) ? '' : @file_get_contents($file);
}

// 递归创建文件夹
function mk_dirs($path, int $mode = 0777)
{
    if (!is_dir(dirname($path))) {
        mk_dirs(dirname($path));
    }

    if (!file_exists($path)) {
        return mkdir($path, $mode);
    }

    return true;
}
// 写文件
function write_file($file, $content)
{
    $dir = dirname($file);
    if (!is_dir($dir)) {
        mk_dirs($dir);
    }
    return @file_put_contents($file, $content);
}
// 目录拷贝
function copy_dir(string $source, string $dest, bool $overwrite = false)
{
    if (is_dir($source)) {
        if (!is_dir($dest)) {
            mkdir($dest);
        }
        $files = scandir($source);
        foreach ($files as $file) {
            if ($file !== "." && $file !== "..") {
                copy_dir("$source/$file", "$dest/$file", $overwrite);
            }
        }
    } else if (file_exists($source) && ($overwrite || !file_exists($dest))) {
        copy($source, $dest);
    }
}
//  删除目录
function remove_dir(string $dir)
{
    if (is_link($dir) || is_file($dir)) {
        return unlink($dir);
    }
    $files = array_diff(scandir($dir), array('.', '..'));
    foreach ($files as $file) {
        (is_dir("$dir/$file") && !is_link($dir)) ? remove_dir("$dir/$file") : unlink("$dir/$file");
    }
    return rmdir($dir);
}

/**
 * @param string $lock_file composer.lock文件绝对地址
 * @param string $zip_dest_dir zip文件目标目录,没有会自动生成
 * @return void
 */
function run(string $lock_file, string $zip_dest_dir)
{
    $src = dirname($lock_file) . "/vendor";
    $json_content = json_decode(read_file($lock_file), true);
    $packages = $json_content["packages"];
    foreach ($packages as $package) {
        $name = $package["name"];
        $version = $package["version"];
        $target_dir = $zip_dest_dir . "/vendor-temp/$name"; // 在目标文件夹下创建vendor-temp缓存目录
        mk_dirs($target_dir);

        echo "start -- package name is $name,version is $version \n";
        $filenames = glob("$src/$name/composer.json");
        foreach ($filenames as $filename) {
            if (is_file($filename)) {
                $lib_src_dir = dirname($filename);
                copy_dir($lib_src_dir, $target_dir, true);
                $filename = "$target_dir/composer.json";
                writeOneVersion($filename, $version);
                archiveOne($filename, $zip_dest_dir);
            }
        }
        echo "end -- package name is $name,version is $version \n\n";
        // break;
    }

    chdir($zip_dest_dir); // 必须切换当前目录,否则remove_dir删除不了
    remove_dir($zip_dest_dir . "/vendor-temp"); // 全部结束后,删除vendor-temp目录
}

/**
 * 给一个composer.json文件写入版本信息
 * @param string $filename
 * @param string $version
 * @return void
 */
function writeOneVersion(string $filename, string $version)
{
    $obj = json_decode(read_file($filename));
    $obj->version = $version;

    $str = json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
    write_file($filename, $str);
}

/**
 * 打包一个包到目标目录
 * @param string $filename 包目录的composer.json
 * @param string $dest 目标目录
 * @return void
 */
function archiveOne(string $filename, string $dest)
{
    $dir = dirname($filename);
    chdir($dir);
    echo "current dir is " . getcwd() . "\n";
    passthru("composer archive --format=zip --dir=$dest");
}

2.生成离线仓库

  • 添加composer 到环境变量中

  • 修改 php 文件中的main方法的参数地址

  • 其中,$lock_file 是 composer.lock文件绝对地址,用于提取包的版本信息

  • $zip_dest_dir 是存放离线zip文件的目录

    修改完后运行php

php generate_repositories.php

生成的zip目录中,vendor-temp目录是临时用于写入版本信息的,每次生成都会被同一个包的不同版本重新覆盖,只有zip目录下的包才是不重复的,因为生成时会区分版本信息。

3.其他项目使用

其他项目修改 composer.json文件,需要配置离线仓库目录,
composer.json 离线配置参考,重点是 repositories 配置

{
  "minimum-stability": "dev",
  "require": {
    "php": ">=8.0",
    "workerman/webman-framework": "^1.5.0",
    "monolog/monolog": "^2.0",
    "ext-pdo": "*",
    "webman/admin": "^0.6.24",
    "webman/console": "^1.3"
  },
  "repositories": [
    {
      "packagist.org": false
    },
    {
      "type": "artifact",
      "url": "D:/3PHP/repos/zips/"
    }
  ]
}

"packagist.org": false 关闭去互联网请求
下面就是配置刚才生成的zip包目录地址

然后,就可以使用正常的
composer require webman/admin
来安装依赖了

常见故障

生成zip包失败

如果遇到某些包没有不成功的,就是刚才生成zip时,没有生成成功,大概是因为那个包的composer.json文件写的不规范,导致 composer
archive 不成功
可以查看输出时报红的信息,current package name is 这个包后面的就是当前错误的包,然后手动去这个包目录下执行

composer archive --format=zip

就会提示错误信息,对应修改 composer.json 文件即可,生成后,手动复制到 zip目标目录下。

389 2 0
2个评论

释永战

不错的思路,学习一下

  • 暂无评论
软饭工程师

我之前也有这样的场景,是在有网环境把扩展安装好,然后带进无网环境,替换composer.lock 文件和vendor 目录下

  • 暂无评论

Ace华

760
积分
0
获赞数
0
粉丝数
2023-02-14 加入
×
🔝