임포트 경로 해석

임포트 경로 해석 (Import Path Resolution)

Solidity 컴파일러는 모든 플랫폼에서 재현 가능한 빌드를 지원하기 위해, 소스 파일이 저장된 파일시스템의 세부 사항을 추상화해요. 임포트에 사용되는 경로는 어디서나 똑같이 동작해야 하고, 반면 명령줄 인터페이스는 좋은 사용자 경험을 위해 플랫폼 특화 경로를 다룰 수 있어야 해요. 이 절은 Solidity가 이 두 요구를 어떻게 조화시키는지 상세히 설명해요.

출처: 문서

본문

가상 파일시스템 (Virtual Filesystem)

컴파일러는 내부 데이터베이스(가상 파일시스템, VFS)를 유지하며, 각 소스 유닛에 고유한 소스 유닛 이름(source unit name)이 할당돼요. 이 이름은 불투명하고 구조화되지 않은 식별자예요. import 문을 사용하면 소스 유닛 이름을 참조하는 임포트 경로(import path)를 지정하게 돼요.

임포트 콜백 (Import Callback)

VFS는 처음에는 컴파일러가 입력으로 받은 파일만으로 채워져요. 추가 파일은 컴파일 중에 임포트 콜백(import callback)을 통해 로드될 수 있는데, 이는 사용하는 컴파일러 종류에 따라 달라요(아래 참고).

컴파일러가 VFS에서 임포트 경로와 일치하는 소스 유닛 이름을 찾지 못하면 콜백을 호출해요. 콜백은 그 이름 아래에 놓일 소스 코드를 얻는 책임이 있어요. 임포트 콜백은 소스 유닛 이름을 경로가 아니라 자유롭게 해석할 수 있어요.

필요할 때 사용 가능한 콜백이 없거나, 콜백이 소스 코드를 찾는 데 실패하면 컴파일이 실패해요.

기본적으로 명령줄 컴파일러는 소스 유닛 이름을 로컬 파일시스템의 경로로 해석하는 기초적인 콜백인 호스트 파일시스템 로더(Host Filesystem Loader)를 제공해요. 이 콜백은 --no-import-callback 명령줄 옵션으로 비활성화할 수 있어요. JavaScript 인터페이스는 기본적으로 어떤 것도 제공하지 않지만 사용자가 제공할 수 있어요.

이 메커니즘은 로컬 파일시스템(컴파일러가 브라우저에서 실행될 때처럼 접근할 수 없는 경우도 있음)이 아닌 곳에서 소스 코드를 얻는 데 사용될 수 있어요. 예를 들어 Remix IDE는 HTTP, IPFS, Swarm URL에서 파일을 가져오거나 NPM 레지스트리의 패키지를 직접 참조하게 해주는 다재다능한 콜백을 제공해요.

참고 (Note)

호스트 파일시스템 로더의 파일 조회는 플랫폼 의존적이에요. 예를 들어 소스 유닛 이름의 백슬래시가 디렉토리 구분자로 해석되거나 해석되지 않을 수 있고, 조회가 대소문자를 구분하거나 구분하지 않을 수 있어요. 이는 기본 플랫폼에 따라 달라져요.

이식성을 위해 특정 임포트 콜백이나 특정 플랫폼에서만 올바르게 동작하는 임포트 경로는 피하는 걸 권장해요. 예를 들어 백슬래시를 지원하는 플랫폼에서도 경로 구분자로 동작하는 정방향 슬래시(forward slash)를 항상 사용해야 해요.

가상 파일시스템의 초기 내용 (Initial Content of the Virtual Filesystem)

VFS의 초기 내용은 컴파일러를 어떻게 호출하는지에 따라 달라져요:

  • solc / 명령줄 인터페이스: 컴파일러의 명령줄 인터페이스로 파일을 컴파일할 때 Solidity 코드가 담긴 파일 경로를 하나 이상 제공해요: solc contract.sol /usr/local/dapp-bin/token.sol. 이런 방식으로 로드된 파일의 소스 유닛 이름은 해당 경로를 표준 형태로 변환하고, 가능하면 기본 경로(base path)나 포함 경로(include path) 중 하나에 상대적으로 만들어서 구성돼요. 이 과정에 대한 자세한 설명은 CLI 경로 정규화와 제거(CLI Path Normalization and Stripping)를 참고해요.
  • 표준 JSON: 표준 JSON API(JavaScript 인터페이스나 --standard-json 명령줄 옵션을 통해)를 사용하면 JSON 형식으로 입력을 제공하며, 그 안에 모든 소스 파일의 내용이 포함돼요:
{
  "language": "Solidity",
  "sources": {
    "contract.sol": { "content": "import \"./util.sol\";\ncontract C {}" },
    "util.sol": { "content": "library Util {}" },
    "/usr/local/dapp-bin/token.sol": { "content": "contract Token {}" }
  },
  "settings": {
    "outputSelection": {
      "*": { "*": ["metadata", "evm.bytecode"] }
    }
  }
}

sources 딕셔너리가 가상 파일시스템의 초기 내용이 되고, 그 키가 소스 유닛 이름으로 사용돼요.

  • 표준 JSON (임포트 콜백을 통한): 표준 JSON에서는 컴파일러에 임포트 콜백을 사용해 소스 코드를 얻으라고 지시하는 것도 가능해요:
{
  "language": "Solidity",
  "sources": {
    "/usr/local/dapp-bin/token.sol": {
      "urls": ["/projects/mytoken.sol", "https://example.com/projects/mytoken.sol"]
    }
  },
  "settings": {
    "outputSelection": {
      "*": { "*": ["metadata", "evm.bytecode"] }
    }
  }
}

임포트 콜백을 사용할 수 있으면, 컴파일러는 한 번 성공적으로 로드되거나 목록 끝에 도달할 때까지 urls에 지정된 문자열을 하나씩 콜백에 전달해요. 소스 유닛 이름은 content를 사용할 때와 같은 방식으로 결정돼요. 즉 sources 딕셔너리의 키이며, urls의 내용은 그것에 전혀 영향을 주지 않아요.

  • 표준 입력: 명령줄에서 소스를 컴파일러의 표준 입력으로 보내 제공할 수도 있어요: echo 'import "./util.sol"; contract C {}' | solc -. 인자로 사용된 -는 컴파일러에게 표준 입력의 내용을 특별한 소스 유닛 이름 <stdin> 아래 가상 파일시스템에 넣으라고 지시해요.

VFS가 초기화된 후에도 추가 파일은 임포트 콜백을 통해서만 추가될 수 있어요.

임포트 (Imports)

import 문은 임포트 경로를 지정해요. 임포트 경로를 어떻게 지정하느냐에 따라 임포트를 두 범주로 나눌 수 있어요:

  • 직접 임포트(Direct imports): 전체 소스 유닛 이름을 직접 지정해요.
  • 상대 임포트(Relative imports): ./ 또는 ../로 시작하는 경로를 가져와서 임포트하는 파일의 소스 유닛 이름과 결합해요.

open in Remix

import "./math/math.sol";
import "contracts/tokens/token.sol";

위에서 ./math/math.sol과 contracts/tokens/token.sol은 임포트 경로이고, 그것들이 변환되는 소스 유닛 이름은 각각 contracts/math/math.sol과 contracts/tokens/token.sol이에요.

직접 임포트 (Direct Imports)

./ 또는 ../로 시작하지 않는 임포트는 직접 임포트예요.

open in Remix

import "/project/lib/util.sol";         // source unit name: /project/lib/util.sol
import "lib/util.sol";                  // source unit name: lib/util.sol
import "@openzeppelin/address.sol";     // source unit name: @openzeppelin/address.sol
import "https://example.com/token.sol"; // source unit name: https://example.com/token.sol

임포트 리매핑을 적용한 후에는 임포트 경로가 그대로 소스 유닛 이름이 돼요.

참고 (Note)

소스 유닛 이름은 단지 식별자이며, 그 값이 우연히 경로처럼 보여도 셸에서 일반적으로 기대하는 정규화 규칙을 받지 않아요. /./나 /../ 세그먼트 또는 여러 개의 슬래시 시퀀스도 그 일부로 남아요.

표준 JSON 인터페이스를 통해 소스를 제공할 때는 디스크에서 같은 파일을 가리키는 소스 유닛 이름들에 서로 다른 내용을 연관시키는 것도 완전히 가능해요.

소스가 가상 파일시스템에 없으면 컴파일러는 소스 유닛 이름을 임포트 콜백에 전달해요. 호스트 파일시스템 로더는 그것을 경로로 사용해 디스크에서 파일을 찾으려고 해요. 이 시점에 플랫폼 특화 정규화 규칙이 작동하고, VFS에서 서로 다르게 간주되던 이름이 실제로는 같은 파일이 로드되는 결과를 낼 수 있어요. 예를 들어 /project/lib/math.sol과 /project/lib/../lib///math.sol은 디스크에서 같은 파일을 가리키는데도 VFS에서는 완전히 다르게 간주돼요.

임포트 콜백이 결국 디스크의 같은 파일에서 두 개의 서로 다른 소스 유닛 이름에 대한 소스 코드를 로드해도, 컴파일러는 여전히 그것들을 분리된 소스 유닛으로 봐요. 중요한 것은 코드의 물리적 위치가 아니라 소스 유닛 이름이에요.

상대 임포트 (Relative Imports)

./ 또는 ../로 시작하는 임포트는 상대 임포트예요. 이런 임포트는 임포트하는 소스 유닛의 소스 유닛 이름에 상대적인 경로를 지정해요:

open in Remix

import "./util.sol" as util;    // source unit name: /project/lib/util.sol
import "../token.sol" as token; // source unit name: /project/token.sol

open in Remix

import "./util.sol" as util;    // source unit name: lib/util.sol
import "../token.sol" as token; // source unit name: token.sol

참고 (Note)

상대 임포트는 항상 ./나 ../로 시작해요. 그래서 import "util.sol"은 import "./util.sol"과 달리 직접 임포트예요. 두 경로 모두 호스트 파일시스템에서는 상대적으로 간주되지만, util.sol은 실제로 VFS에서는 절대적이에요.

경로 세그먼트(path segment)를 구분자를 포함하지 않고 두 개의 경로 구분자로 둘러싸인 경로의 비어 있지 않은 부분으로 정의해 볼게요. 구분자는 정방향 슬래시 또는 문자열의 시작/끝이에요. 예를 들어 ./abc/..//에는 세 개의 경로 세그먼트가 있어요: ., abc, ...

컴파일러는 다음 방식으로 임포트를 임포트 경로에 기반해 소스 유닛 이름으로 해석해요:

  • 임포트하는 소스 유닛의 소스 유닛 이름에서 시작해요.
  • 앞에 슬래시가 붙은 마지막 경로 세그먼트를 해석된 이름에서 제거해요.
  • 그다음, 임포트 경로의 각 세그먼트에 대해 맨 왼쪽부터: 세그먼트가 .이면 건너뛰어요. 세그먼트가 ..이면 앞에 슬래시가 붙은 마지막 경로 세그먼트를 해석된 이름에서 제거해요. 그 외에는 세그먼트(해석된 이름이 비어 있지 않으면 단일 슬래시가 앞에 붙은)를 해석된 이름에 추가해요.

앞에 슬래시가 붙은 마지막 경로 세그먼트의 제거는 다음과 같이 동작한다고 이해해요:

  • 마지막 슬래시 이후의 모든 것을 제거해요(즉 a/b//c.sol이 a/b//이 됨).
  • 모든 끝 슬래시를 제거해요(즉 a/b//이 a/b가 됨).

이 과정은 임포트 경로에서 나오는 해석된 소스 유닛 이름의 부분을 UNIX 경로의 일반 규칙(즉 모든 .과 ..이 제거되고 여러 슬래시가 하나로 압축됨)에 따라 정규화한다는 점에 주의해요. 반면 임포트하는 모듈의 소스 유닛 이름에서 나오는 부분은 정규화되지 않은 채 남아요. 이는 임포트하는 파일이 URL로 식별되는 경우 protocol:// 부분이 protocol:/로 변하지 않도록 보장해요.

임포트 경로가 이미 정규화되어 있다면 위 알고리즘이 아주 직관적인 결과를 낼 거라고 기대할 수 있어요. 정규화되어 있지 않을 때 기대할 수 있는 몇 가지 예는 다음과 같아요:

open in Remix

import "./util/./util.sol";         // source unit name: lib/src/../util/util.sol
import "./util//util.sol";          // source unit name: lib/src/../util/util.sol
import "../util/../array/util.sol"; // source unit name: lib/src/array/util.sol
import "../.././../util.sol";       // source unit name: util.sol
import "../../.././../util.sol";    // source unit name: util.sol

참고 (Note)

앞에 .. 세그먼트를 포함한 상대 임포트의 사용은 권장되지 않아요. 기본 경로와 포함 경로를 사용한 직접 임포트로 같은 효과를 더 안정적으로 얻을 수 있어요.

기본 경로와 포함 경로 (Base Path and Include Paths)

기본 경로와 포함 경로는 호스트 파일시스템 로더가 파일을 로드할 디렉토리를 나타내요. 소스 유닛 이름이 로더에 전달되면, 로더는 기본 경로를 그 앞에 붙이고 파일시스템 조회를 수행해요. 조회가 성공하지 않으면 포함 경로 목록의 모든 디렉토리에 대해 같은 작업을 해요.

기본 경로를 프로젝트의 루트 디렉토리로 설정하고, 포함 경로로 프로젝트가 의존하는 라이브러리가 있을 수 있는 추가 위치를 지정하는 걸 권장해요. 이렇게 하면 라이브러리가 파일시스템 어디에 있든 프로젝트에 상대적으로 똑같이 임포트할 수 있어요.

예를 들어 npm으로 패키지를 설치하고 컨트랙트가 @openzeppelin/contracts/utils/Strings.sol을 임포트한다면, 이 옵션들로 라이브러리가 npm 패키지 디렉토리 중 하나에서 찾을 수 있다고 컴파일러에 알릴 수 있어요:

solc contract.sol \
    --base-path . \
    --include-path node_modules/ \
    --include-path /usr/local/lib/node_modules/

라이브러리를 로컬 또는 전역 패키지 디렉토리에, 심지어 프로젝트 루트 바로 아래에 설치하더라도 컨트랙트는(정확히 같은 메타데이터로) 컴파일돼요.

기본적으로 기본 경로는 비어 있고, 그러면 소스 유닛 이름이 그대로 남아요. 소스 유닛 이름이 상대 경로일 때 이는 컴파일러가 호출된 디렉토리에서 파일을 찾는 결과를 내요. 또한 이것이 소스 유닛 이름의 절대 경로가 실제로 디스크의 절대 경로로 해석되는 유일한 값이에요. 기본 경로 자체가 상대적이면, 컴파일러의 현재 작업 디렉토리에 상대적인 것으로 해석돼요.

참고 (Note)

포함 경로는 빈 값을 가질 수 없고 비어 있지 않은 기본 경로와 함께 사용해야 해요.

참고 (Note)

포함 경로와 기본 경로는 임포트 해석을 모호하게 만들지 않는 한 겹칠 수 있어요. 예를 들어 기본 경로 안의 디렉토리를 포함 디렉토리로 지정하거나, 다른 포함 디렉토리의 하위 디렉토리인 포함 디렉토리를 가질 수 있어요. 컴파일러는 호스트 파일시스템 로더에 전달된 소스 유닛 이름이 여러 포함 경로, 또는 포함 경로와 기본 경로와 결합했을 때 기존 경로를 나타낼 때만 에러를 발생시켜요.

CLI 경로 정규화와 제거 (CLI Path Normalization and Stripping)

명령줄에서 컴파일러는 다른 어떤 프로그램에서 기대하는 것처럼 동작해요. 플랫폼 고유 형식의 경로를 받고, 상대 경로는 현재 작업 디렉토리에 상대적이에요.

그러나 명령줄에 지정된 경로가 있는 파일에 할당된 소스 유닛 이름은, 프로젝트가 다른 플랫폼에서 컴파일되거나 컴파일러가 다른 디렉토리에서 호출됐다고 해서 바뀌면 안 돼요.

이를 위해 명령줄에서 오는 소스 파일 경로는 표준 형태로 변환되고, 가능하면 기본 경로나 포함 경로 중 하나에 상대적으로 만들어져야 해요. 정규화 규칙은 다음과 같아요:

  • 경로가 상대적이면 현재 작업 디렉토리를 앞에 붙여 절대 경로로 만들어요.
  • 내부의 .과 .. 세그먼트를 접어요.
  • 플랫폼 특화 경로 구분자를 정방향 슬래시로 교체해요.
  • 여러 개의 연속된 경로 구분자 시퀀스를 단일 구분자로 압축해요(UNC 경로의 앞 슬래시인 경우는 제외).
  • 경로에 루트 이름(예: Windows의 드라이브 문자)이 포함되고 그 루트가 현재 작업 디렉토리의 루트와 같으면, 루트를 /로 교체해요.
  • 경로의 심볼릭 링크는 해석하지 않아요. 유일한 예외는 상대 경로를 절대 경로로 만드는 과정에서 앞에 붙는 현재 작업 디렉토리 경로예요. 일부 플랫폼에서는 작업 디렉토리가 항상 심볼릭 링크가 해석된 상태로 보고되므로, 일관성을 위해 컴파일러는 어디서나 심볼릭 링크를 해석해요.
  • 파일시스템이 대소문자를 구분하지 않지만 보존하고 실제 디스크의 대소문자가 다른 경우에도 경로의 원래 대소문자가 보존돼요.

참고 (Note)

경로를 플랫폼 독립적으로 만들 수 없는 상황이 있어요. 예를 들어 Windows에서 컴파일러는 현재 드라이브의 루트 디렉토리를 /로 참조해 드라이브 문자를 피할 수 있지만, 다른 드라이브로 이어지는 경로에는 여전히 드라이브 문자가 필요해요. 모든 파일을 같은 드라이브의 단일 디렉토리 트리 안에서 사용할 수 있게 하면 이런 상황을 피할 수 있어요.

정규화 후 컴파일러는 소스 파일 경로를 상대적으로 만들려고 해요. 먼저 기본 경로를 시도하고, 그다음 주어진 순서대로 포함 경로를 시도해요. 기본 경로가 비어 있거나 지정되지 않으면, 현재 작업 디렉토리의 경로(모든 심볼릭 링크가 해석된)와 같다고 취급돼요. 결과는 정규화된 디렉토리 경로가 정규화된 파일 경로의 정확한 접두사일 때만 받아들여져요. 그렇지 않으면 파일 경로는 절대 경로로 남아요. 이는 변환을 명확하게 하고 상대 경로가 ../로 시작하지 않도록 보장해요. 결과 파일 경로가 소스 유닛 이름이 돼요.

참고 (Note)

제거로 생성된 상대 경로는 기본 경로와 포함 경로 내에서 고유하게 유지되어야 해요. 예를 들어 다음 명령에서 /project/contract.sol과 /lib/contract.sol 둘 다 존재하면 컴파일러는 에러를 발생시켜요:

solc /project/contract.sol --base-path /project --include-path /lib

참고 (Note)

버전 0.8.8 이전에는 CLI 경로 제거가 수행되지 않았고 적용된 유일한 정규화는 경로 구분자의 변환이었어요. 더 오래된 컴파일러 버전으로 작업할 때는 기본 경로에서 컴파일러를 호출하고 명령줄에서 상대 경로만 사용하는 걸 권장해요.

허용 경로 (Allowed Paths)

보안 조치로, 호스트 파일시스템 로더는 기본적으로 안전하다고 간주되는 몇몇 위치 밖의 파일 로드를 거부해요:

  • 표준 JSON 모드 밖: 명령줄에 나열된 입력 파일을 포함하는 디렉토리. 리매핑 대상으로 사용되는 디렉토리(대상이 디렉토리가 아니면, 즉 /, /., /..로 끝나지 않으면 대상이 들어 있는 디렉토리를 대신 사용). 기본 경로와 포함 경로.
  • 표준 JSON 모드: 기본 경로와 포함 경로.

--allow-paths 옵션으로 추가 디렉토리를 화이트리스트에 넣을 수 있어요. 이 옵션은 쉼표로 구분된 경로 목록을 받아요:

cd /home/user/project/
solc token/contract.sol \
    lib/util.sol=libs/util.sol \
    --base-path=token/ \
    --include-path=/lib/ \
    --allow-paths=../utils/,/tmp/libraries

컴파일러가 위 명령으로 호출되면, 호스트 파일시스템 로더는 다음 디렉토리에서 파일 임포트를 허용해요:

  • /home/user/project/token/ (token/이 입력 파일을 포함하고 기본 경로이기 때문).
  • /lib/ (/lib/이 포함 경로 중 하나이기 때문).
  • /home/user/project/libs/ (libs/이 리매핑 대상을 포함하는 디렉토리이기 때문).
  • /home/user/utils/ (--allow-paths에 전달된 ../utils/ 때문).
  • /tmp/libraries/ (--allow-paths에 전달된 /tmp/libraries 때문).

참고 (Note)

컴파일러의 작업 디렉토리는 우연히 기본 경로(또는 기본 경로가 지정되지 않았거나 빈 값을 가진)일 때만 기본적으로 허용되는 경로 중 하나예요.

참고 (Note)

컴파일러는 허용 경로가 실제로 존재하고 디렉토리인지 확인하지 않아요. 존재하지 않거나 빈 경로는 그냥 무시돼요. 허용 경로가 디렉토리가 아니라 파일과 일치하면, 그 파일도 화이트리스트에 포함된 것으로 간주돼요.

참고 (Note)

허용 경로는 파일시스템이 대소문자를 구분하지 않아도 대소문자를 구분해요. 대소문자는 임포트에서 사용하는 것과 정확히 일치해야 해요. 예를 들어 --allow-paths tokens는 import "Tokens/IERC20.sol"와 일치하지 않아요.

경고 (Warning)

허용된 디렉토리의 심볼릭 링크로만 도달할 수 있는 파일과 디렉토리는 자동으로 화이트리스트에 포함되지 않아요. 예를 들어 위 예시에서 token/contract.sol이 실제로 /etc/passwd를 가리키는 심볼릭 링크라면, /etc/도 허용 경로 중 하나가 아니면 컴파일러는 그 파일 로드를 거부해요.

임포트 리매핑 (Import Remapping)

임포트 리매핑은 임포트를 가상 파일시스템의 다른 위치로 리다이렉트할 수 있게 해줘요. 이 메커니즘은 임포트 경로와 소스 유닛 이름 사이의 변환을 바꿔서 동작해요.

예를 들어 가상 디렉토리 github.com/ethereum/dapp-bin/library/에서 오는 임포트가 dapp-bin/library/에서 오는 것으로 보이도록 리매핑을 설정할 수 있어요.

컨텍스트(context)를 지정해 리매핑의 범위를 제한할 수 있어요. 이는 특정 라이브러리나 특정 파일에 위치한 임포트에만 적용되는 리매핑을 만들 수 있게 해줘요. 컨텍스트 없이 리매핑은 가상 파일시스템의 모든 파일에서 일치하는 모든 임포트에 적용돼요.

임포트 리매핑은 context:prefix=target 형태를 가져요:

  • context는 임포트를 포함하는 파일의 소스 유닛 이름의 시작과 일치해야 해요.
  • prefix는 임포트에서 나오는 소스 유닛 이름의 시작과 일치해야 해요.
  • target은 prefix가 교체되는 값이에요.

예를 들어 https://github.com/ethereum/dapp-bin/을 로컬의 /project/dapp-bin에 클론하고 컴파일러를 다음과 같이 실행하면:

solc github.com/ethereum/dapp-bin/=dapp-bin/ --base-path /project source.sol

소스 파일에서 다음을 사용할 수 있어요:

open in Remix

import "github.com/ethereum/dapp-bin/library/math.sol"; // source unit name: dapp-bin/library/math.sol

컴파일러는 VFS의 dapp-bin/library/math.sol에서 파일을 찾을 거예요. 파일이 거기에 없으면 소스 유닛 이름이 호스트 파일시스템 로더에 전달되고, 로더는 /project/dapp-bin/library/math.sol을 찾을 거예요.

경고 (Warning)

리매핑에 대한 정보는 컨트랙트 메타데이터에 저장돼요. 컴파일러가 생성한 바이너리에는 메타데이터의 해시가 새겨져 있으므로, 리매핑에 대한 어떤 수정도 다른 바이트코드를 만들게 돼요.

이런 이유로 리매핑 대상에 로컬 정보를 포함하지 않도록 주의해야 해요. 예를 들어 라이브러리가 /home/user/packages/mymath/math.sol에 있다면 @math/=/home/user/packages/mymath/ 같은 리매핑은 홈 디렉토리가 메타데이터에 포함되는 결과를 내요. 다른 머신에서 그런 리매핑으로 같은 바이트코드를 재현하려면, VFS에(그리고 호스트 파일시스템 로더에 의존한다면 호스트 파일시스템에도) 로컬 디렉토리 구조의 일부를 재현해야 해요.

로컬 디렉토리 구조가 메타데이터에 박히는 것을 피하려면, 라이브러리를 포함하는 디렉토리를 포함 경로로 지정하는 걸 권장해요. 예를 들어 위 예시에서 --include-path /home/user/packages/는 mymath/로 시작하는 임포트를 사용할 수 있게 해줘요. 리매핑과 달리 그 옵션만으로는 mymath를 @math처럼 보이게 하지 않지만, 심볼릭 링크를 만들거나 패키지 하위 디렉토리를 이름을 바꿔서 이룰 수 있어요.

더 복잡한 예로, /project/dapp-bin_old에 체크아웃한 옛 버전의 dapp-bin을 사용하는 모듈에 의존한다고 가정해 볼게요. 그러면 다음과 같이 실행할 수 있어요:

solc module1:github.com/ethereum/dapp-bin/=dapp-bin/ \
     module2:github.com/ethereum/dapp-bin/=dapp-bin_old/ \
     --base-path /project \
     source.sol

이는 module2의 모든 임포트가 옛 버전을 가리키지만 module1의 임포트는 새 버전을 가리킨다는 뜻이에요.

리매핑 동작을 지배하는 상세 규칙은 다음과 같아요:

  • 리매핑은 임포트 경로와 소스 유닛 이름 사이의 변환에만 영향을 줘요. 다른 어떤 방식으로 VFS에 추가된 소스 유닛 이름은 리매핑될 수 없어요. 예를 들어 명령줄에 지정한 경로와 표준 JSON의 sources.urls의 경로는 영향을 받지 않아요. solc /project/ = /contracts/ /project/contract.sol # source unit name: /project/contract.sol — 위 예시에서 컴파일러는 /project/contract.sol에서 소스 코드를 로드하고, /contract/contract.sol이 아니라 정확히 그 소스 유닛 이름 아래 VFS에 배치해요.
  • 컨텍스트와 prefix는 임포트 경로가 아니라 소스 유닛 이름과 일치해야 해요. 즉 ./이나 ../를 직접 리매핑할 수는 없어요. 소스 유닛 이름으로 변환되는 동안 교체되기 때문이에요. 하지만 그들이 교체되는 이름의 일부는 리매핑할 수 있어요. solc ./ = a/ /project/ = b/ /project/contract.sol # source unit name: /project/contract.sol — /project/contract.sol에서 import "./util.sol" as util;는 // source unit name: b/util.sol이 돼요. 기본 경로나 임포트 콜백이 내부적으로만 추가하는 다른 경로 부분은 리매핑할 수 없어요. solc /project/ = /contracts/ /project/contract.sol --base-path /project # source unit name: contract.sol — import "util.sol" as util; // source unit name: util.sol.
  • 대상은 소스 유닛 이름에 직접 삽입되며 반드시 유효한 경로일 필요는 없어요. 임포트 콜백이 처리할 수 있는 한 무엇이든 될 수 있어요. 호스트 파일시스템 로더의 경우 상대 경로도 포함해요. JavaScript 인터페이스를 사용할 때는 콜백이 처리할 수 있다면 URL과 추상 식별자도 사용할 수 있어요.
  • 리매핑은 상대 임포트가 이미 소스 유닛 이름으로 해석된 후에 일어나요. 즉 ./과 ../로 시작하는 대상은 특별한 의미가 없고, 소스 파일의 위치가 아니라 기본 경로에 상대적이에요.
  • 리매핑 대상은 정규화되지 않아요. 그래서 @root/=./a/b//는 @root/contract.sol을 a/b/contract.sol이 아니라 ./a/b//contract.sol로 리매핑해요.
  • 대상이 슬래시로 끝나지 않으면 컴파일러는 자동으로 하나를 추가하지 않아요. solc /project/ = /contracts /project/contract.sol # source unit name: /project/contract.sol — import "/project/util.sol" as util; // source unit name: /contractsutil.sol.
  • 컨텍스트와 prefix는 패턴이고 일치는 정확해야 해요. a//b=c는 a/b와 일치하지 않아요. 소스 유닛 이름은 정규화되지 않으므로 a/b=c도 a//b와 일치하지 않아요. 파일과 디렉토리 이름의 일부도 일치할 수 있어요. /newProject/con:/new=old는 /newProject/contract.sol과 일치해 oldProject/contract.sol로 리매핑해요.
  • 단일 임포트에는 최대 하나의 리매핑이 적용돼요. 여러 리매핑이 같은 소스 유닛 이름과 일치하면, 가장 긴 일치 컨텍스트를 가진 것이 선택돼요. 컨텍스트가 동일하면 가장 긴 일치 prefix를 가진 것이 선택돼요. 컨텍스트와 prefix가 동일하면 마지막에 지정된 것이 이겨요. 리매핑은 다른 리매핑에 동작하지 않아요. 예를 들어 a=b b=c c=d는 a가 d로 리매핑되는 결과를 내지 않아요.
  • prefix는 비어 있을 수 없지만 컨텍스트와 대상은 선택적이에요. 대상이 빈 문자열이면 prefix는 단순히 임포트 경로에서 제거돼요. 빈 컨텍스트는 그 리매핑이 모든 소스 유닛의 모든 임포트에 적용된다는 뜻이에요.

임포트에서 URL 사용하기 (Using URLs in imports)

https://나 data:// 같은 대부분의 URL 접두사는 임포트 경로에서 특별한 의미가 없어요. 유일한 예외는 file://이며, 호스트 파일시스템 로더가 소스 유닛 이름에서 제거해요.

로컬에서 컴파일할 때 임포트 리매핑을 사용해 프로토콜과 도메인 부분을 로컬 경로로 교체할 수 있어요:

solc :https://github.com/ethereum/dapp-bin=/usr/local/dapp-bin contract.sol

리매핑 컨텍스트가 비어 있을 때 필요한 앞의 :에 주의해요. 그렇지 않으면 https: 부분이 컴파일러에 의해 컨텍스트로 해석될 거예요.

더 알아보기 (Learn more)