一度のエラー履歴を記入する-npmキャッシュの概要

15365 ワード

に縁を付ける


一度インストールプロジェクトに依存したとき、端末は以下のエラーを報告し、依存インストールに失敗した.
エラーメッセージから@sentry/cliというパッケージの原因がわかります.プロジェクトではこのパッケージに直接依存していないため、パッケージ間の影響を排除するためにフォルダを新設し、このパッケージを単独でインストールしたところ、同じエラーが発生したことがわかりました.それから同僚にこのかばんをインストールしてみて、すべて正常で、間違いを報告していないことに気づきました.
次は、google検索、github issue検索、npmインストール、npmソースの切り替え、nodeバージョンの切り替え、別のバージョン@sentry/cliのインストール、yarnとnpmのキャッシュのクリア、コンピュータの再起動です.しかし、卵用は何もないことがわかりました..

物事はそんなに簡単ではないようだ。


エラーメッセージを振り返ると、node scripts/install.jsを実行中に発生したエラーであることがわかりますので、コードを引っ張ってローカルに走ってみましょう.やると言ったらやる、@sentry/cli cloneを現地に着いたら、まず依存をインストールして、それからnode scripts/install.jsを実行して以下のエラーを発見します.
実際に/Users/sliwey/githome/sentry-cli/sentry-cli --versionコマンドを実行中に発生したエラーを発見し、上記のパスに基づいてプロジェクトルートディレクトリの下にsentry-cliという実行可能ファイルが1つ追加されたことを発見しました.
だからこのファイルに問題があるはずですが、このファイルはどこから来たのでしょうか.scripts/install.jsのコードを見てみると、実は一つのことをしていました.
    downloadBinary()
      .then(() => checkVersion())
      .then(() => process.exit(0))
      .catch(e => {
        console.error(e.toString());
        process.exit(1);
      });

実行可能なファイルをダウンロードし、次のバージョン番号を確認します.checkVersionまず表を押さないで、重点ではありませんて、ただ下のバージョン番号を判断して、downloadBinary(私はコードを簡略化して、少し注釈を加えて、具体的なコードはgithub.com/getsentry/sを見ることができます...):
    function downloadBinary() {
      const arch = os.arch();
      const platform = os.platform();
      const outputPath = helper.getPath();
    
      //                
      const downloadUrl = getDownloadUrl(platform, arch);
    
      //             
      const cachedPath = getCachedPath(downloadUrl);
    
      //     ,            
      if (fs.existsSync(cachedPath)) {
        copyFileSync(cachedPath, outputPath);
        return Promise.resolve();
      }
    
      //      ,   ,        
      return fetch(downloadUrl, { redirect: 'follow', agent }).then(response => {
        const tempPath = getTempFile(cachedPath);
        mkdirp.sync(path.dirname(tempPath));
    
        return new Promise((resolve, reject) => {
          response.body
            .pipe(fs.createWriteStream(tempPath, { mode: '0755' }))
        }).then(() => {
          copyFileSync(tempPath, cachedPath);
          copyFileSync(tempPath, outputPath);
          fs.unlinkSync(tempPath);
        });
      });
    }

さっきのローカルの実行状況から見ると、ダウンロードは行われていません.その実行可能ファイルがキャッシュから取られていることがわかります.では、ブレークポイントを打ってキャッシュパスを見てみましょう.
得られたパスに従って、対応するファイルを削除し、再インストールし、everything is ok~

次がポイント


問題は解決しましたが、yarnとnpmを含むキャッシュクリアが行われたことを思い出すと、次の2つのコマンドで行われていました.
    yarn cache clean
    npm cache clean --force

上で得られたキャッシュパスから,sentry-cli~/.npmフォルダの下にキャッシュされていることが分かるのでyarnとは関係ないはずで,先に排除する.そしてnpmを見ると、npm cache clean --forceでキャッシュをクリアし、~/.npmフォルダの下のファイルをクリアしていないことがわかりました.では、このコマンドはどこをクリアしますか?まずドキュメントを見てみましょう.npm-cache
読みやすいように、いくつかの図を切りました.
一見何の欠点もないようで、自分のcache構成をチェックしても異常は見つかりませんでした.
では、いったいどこの問題なのか、ソースコードしか見られないようです.目標は直接的で、npmのcacheに関連するコードを見つけて、cleanメソッドの実現を直接見ます(具体的なコードはlib/cache.jsを見ることができます):
    function clean (args) {
      if (!args) args = []
      if (args.length) {
        return BB.reject(new Error('npm cache clear does not accept arguments'))
      }
    
      //     
      // npm.cache   ~/.npm
      //   cachePath      ~/.npm/_cacache
      const cachePath = path.join(npm.cache, '_cacache')
      if (!npm.config.get('force')) {
        return BB.reject(new Error("As of npm@5, the npm cache self-heals from corruption issues and data extracted from the cache is guaranteed to be valid. If you want to make sure everything is consistent, use 'npm cache verify' instead. On the other hand, if you're debugging an issue with the installer, you can use `npm install --cache /tmp/empty-cache` to use a temporary cache instead of nuking the actual one.

If you're sure you want to delete the entire cache, rerun this command with --force."
)) } // TODO - remove specific packages or package versions return rm(cachePath) }

これを見ると分かりますが、npm cache clean --force~/.npm/_cacacheフォルダのデータです.
考えてみれば、この点はドキュメントの中で言わないわけにはいかないでしょう.もう一度ドキュメントを見てみると、内容が漏れていました.の
内容は次のとおりです.
簡単に言えば、npm@5の後、npmは、プロファイル内のcacheフィールド構成のパスの下にある_cacacheフォルダにキャッシュデータを格納する.上記の2つのドキュメントの内容を組み合わせると、次のようになります.
  • プロファイルのcacheフィールドは、ルートディレクトリ
  • を構成する.
  • キャッシュデータはルートディレクトリの_cacacheフォルダの
  • に格納.
  • cleanコマンドクリアは_cacacheフォルダ
  • である.

    npmキャッシュに何が保存されているのか

    _cacacheフォルダを開くと、node_modulesの中のように一つ一つのバッグではなく、こうなっています.
    開くとcontent-v2の中には基本的にいくつかのバイナリファイルがあり、バイナリファイルの拡張子を.tgzに変更して解凍した後、私たちがよく知っているnpmパッケージであることがわかります.index-v5の中にはいくつかの記述的なファイルがあり、content-v2のファイルのインデックスでもあります.よく見るとHTTPの応答ヘッダに似ています.キャッシュ関連の値もあります.
    では、これらのファイルはどのように生成されますか?上記のドキュメントから、npmは主にpacoteでパッケージをインストールしていることがわかりますが、npmがコードでpacoteをどのように使用しているかを見てみましょう.npmは主にpacoteを使用する3つの場所があります.
  • npm install xxx(pacote.extractを介して対応するパケットを対応するnode_modulesの下に解凍する.npmソースエントリ:lib/install/action/extract-worker.js,pacoteソースエントリ:extract.js)
  • npm cache add xxx(pacote.tarball.streamを介して~/.npm/_cacacheにキャッシュデータを追加.npmソースエントリ:lib/cache.js、pacoteソースエントリ:tarball.js#tarballStream)
  • npm pack xxx(pacote.tarball.toFileを介して現在のパスで対応する圧縮ファイルが生成される.npmソースエントリ:lib/pack.js、pacoteソースエントリ:tarball.js#tarballToFile)
  • 上記3つのpacoteの方法を比較すると、その主な依存方法はlib/withTarballStreamであることがわかる.js、コードが多いので、簡略化して、主に中国語の注釈を見ればいいです.
        function withTarballStream (spec, opts, streamHandler) {
          opts = optCheck(opts)
          spec = npa(spec, opts.where)
        
          //      
          const tryFile = (
            !opts.preferOnline &&
            opts.integrity &&
            opts.resolved &&
            opts.resolved.startsWith('file:')
          )
            ? BB.try(() => {
              const file = path.resolve(opts.where || '.', opts.resolved.substr(5))
              return statAsync(file)
                .then(() => {
                  const verifier = ssri.integrityStream({ integrity: opts.integrity })
                  const stream = fs.createReadStream(file)
                    .on('error', err => verifier.emit('error', err))
                    .pipe(verifier)
                  return streamHandler(stream)
            })
            : BB.reject(Object.assign(new Error('no file!'), { code: 'ENOENT' }))
        
          //    reject  ,     
          const tryDigest = tryFile
            .catch(err => {
              if (
                opts.preferOnline ||
              !opts.cache ||
              !opts.integrity ||
              !RETRIABLE_ERRORS.has(err.code)
              ) {
                throw err
              } else {
        	    //   cacache        
                const stream = cacache.get.stream.byDigest(
                  opts.cache, opts.integrity, opts
                )
                stream.once('error', err => stream.on('newListener', (ev, l) => {
                  if (ev === 'error') { l(err) }
                }))
                return streamHandler(stream)
                  .catch(err => {
                    if (err.code === 'EINTEGRITY' || err.code === 'Z_DATA_ERROR') {
                      opts.log.warn('tarball', `cached data for ${spec} (${opts.integrity}) seems to be corrupted. Refreshing cache.`)
                      //      EINTEGRITY Z_DATA_ERROR ,    
                      return cleanUpCached(opts.cache, opts.integrity, opts)
                        .then(() => { throw err })
                    } else {
                      throw err
                    }
                  })
              }
            })
        
          //    reject  ,   
          const trySpec = tryDigest
            .catch(err => {
              if (!RETRIABLE_ERRORS.has(err.code)) {
              // If it's not one of our retriable errors, bail out and give up.
                throw err
              } else {
                return BB.resolve(retry((tryAgain, attemptNum) => {
        
        	      //    ,       npm-registry-fetch    
                  const tardata = fetch.tarball(spec, opts)
                  if (!opts.resolved) {
                    tardata.on('manifest', m => {
                      opts = opts.concat({ resolved: m._resolved })
                    })
                    tardata.on('integrity', i => {
                      opts = opts.concat({ integrity: i })
                    })
                  }
                  return BB.try(() => streamHandler(tardata))
                }, { retries: 1 }))
              }
            })
        
          return trySpec
            .catch(err => {
              if (err.code === 'EINTEGRITY') {
                err.message = `Verification failed while extracting ${spec}:
    ${err.message}`
    } throw err }) }

    上記のコードからpacoteはnpm-registry-fetchに依存してパケットをダウンロードしていることがわかる.npm-registry-fetchのドキュメントを表示すると、要求時にcacheのプロパティを設定できます:npm-registry-fetch#opts.cache cacheの値(npmでは~/.npm/_cacache)が設定されていると、IETF RFC 7234に従って生成されたキャッシュデータが所定の経路で作成されることが分かる.そのrfcのアドレスを開くと、HTTPキャッシュを記述した文書であることがわかるので、本段の冒頭で述べたindex-v5の下の文書も理解しやすい.
    簡単にまとめます.
  • ~/.npm/_cacacheには、いくつかのバイナリファイルと、対応するインデックスが格納されています.
  • npm installの場合、キャッシュがあればpacoteで対応するバイナリファイルを対応するnode_modulesの下に解凍します.
  • npm自体は、キャッシュを消去し、キャッシュの整合性を検証する方法のみを提供し、キャッシュを直接操作する方法は提供されず、cacacheによってこれらのキャッシュデータを操作することができる.

  • 最後に書く


    全体を振り返ってみると、ドキュメントをよく見ることがどんなに重要かがわかります.覚えておいて!覚えておいて!しかし、普段あまり注目されていない点も整理しておくと、収穫もあり、文字の形で記録されていて、振り返るのに便利です.
    原文リンク:github.com/sliwey/blog…