Visual Studio离线安装版制作问题汇总

制作方法

离线包的制作方法主要是用vs_setup --layout LAYOUT_DIR,具体参考微软官方文档,这里不再赘述:
https://docs.microsoft.com/zh-cn/visualstudio/install/create-an-offline-installation-of-visual-studio

VS版本

VS 2015有部分包已经无法从官方下载,返回HTTP 404错误,因此目前已无法制作VS 2015版的离线包。目前经测试VS 2017和VS 2019没有问题。

打包成镜像

如果要把离线包做成iso镜像,由于平级目录太多,导致一些GUI制作工具直接卡死。笔者目前能够在网上找到的方案都是说使用一款叫“Free ISO Creator”的工具。

虽然该工具能顺利制作出iso镜像,但经实测在电脑断开互联网连接,完全离线的情况下还是会出现包下载失败的提示:“在 9 次尝试后,下载以下文件时出现问题”。
下载文件出现问题

根据报错的下载url,在离线包根目录下的Catalog.json中查找线索,找到对应的下载目录,再去验证该目录的存在性。结果发现,由于目录名称太长,已被截断,这就导致安装程序找不到目录而使用网络下载。

找到原因就好办了,笔者的解决办法是将镜像创建为UDF格式,支持长文件名,只是打开光盘列出目录时有点卡。笔者利用机器上安装的WSL(Debian Linux)中的genisoimage命令来生成镜像:

genisoimage -v -V VS2019_COMM -udf -o vs2019-comm.iso /mnt/d/VS2017/comm/

格式:
genisoimage 选项 源文件目录

选项:
-v 显示打包进度
-V 卷标
-udf UDF格式
-o 输出文件名

更新及删除过时的包

当VS有版本更新时,可以再次使用--layout进行新包的下载。根据vs_setup --help给出的提示,可以使用--clean 旧Catalog.json来清除过时的包。更新后应执行清理,否则layout目录会迅速膨胀。不过笔者几经尝试都以失败告终,最后被迫仔细研究Catalog.json文件格式,手动写脚本删除已不再使用的包目录。脚本需要Node JS执行环境,且需支持ES6语法的新版本。

使用方法:

node clean.js LAYOUT_DIR

不指定LAYOUT_DIR则默认为脚本文件所在目录下的layout目录:

#!/usr/bin/env node

const process=require('process');
const path=require('path');
const fs=require('fs');

const layoutDir=path.resolve(path.dirname(process.argv[1]), process.argv[2] || 'layout')
console.log(`Layout Directory: ${layoutDir}`);
if(!fs.existsSync(layoutDir)) {
    console.error('Layout Directory not found');
    process.exit(1);
}

const catalogFile=`${layoutDir}/Catalog.json`;
if(!fs.existsSync(catalogFile)) {
    console.error('Catalog File not exists');
    process.exit(2);
}

let catalog=require(catalogFile);
let packages=catalog.packages;

const pkgNames=packages.map(pkg=>{
    const {id,version,chip,language}=pkg;
    let name=id;
    if(version) {
        name+=`,version=${version}`;
    }
    if(chip) {
        name+=`,chip=${chip}`;
    }
    if(language) {
        name+=`,language=${language}`;
    }
    return name;
});
pkgNames.push('certificates');  // always add certificates dir

packages=null;
catalog=null;

const subDirs=fs.readdirSync(layoutDir,{withFileTypes:true}).filter(ent=>ent.isDirectory()).map(ent=>ent.name);
subDirs.forEach(pkgDir=>{
    if(pkgNames.indexOf(pkgDir)<0) {
        const delDir=`${layoutDir}/${pkgDir}`;
        console.log(`deleting ${delDir}`);
        try {
            fs.rmdirSync(delDir, {recursive:true});
        } catch(ex) {
            console.error(ex);
        }
    }
});
console.log('Done');