
com.oracle.truffle.api.InternalResource Maven / Gradle / Ivy
Show all versions of truffle-api Show documentation
/*
* Copyright (c) 2014, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* The Universal Permissive License (UPL), Version 1.0
*
* Subject to the condition set forth below, permission is hereby granted to any
* person obtaining a copy of this software, associated documentation and/or
* data (collectively the "Software"), free of charge and under any and all
* copyright rights in the Software, and any and all patent rights owned or
* freely licensable by each licensor hereunder covering either (i) the
* unmodified Software as contributed to or provided by such licensor, or (ii)
* the Larger Works (as defined below), to deal in both
*
* (a) the Software, and
*
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
* one is included with the Software each a "Larger Work" to which the Software
* is contributed by such licensors),
*
* without restriction, including without limitation the rights to copy, create
* derivative works of, display, perform, and distribute the Software and make,
* use, sell, offer for sale, import, export, have made, and have sold the
* Software and the Larger Work(s), and to sublicense the foregoing rights on
* either these or other terms.
*
* This license is subject to the following condition:
*
* The above copyright notice and either this complete permission notice or at a
* minimum a reference to the UPL must be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.oracle.truffle.api;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.function.BooleanSupplier;
/**
* Represents an internal resource of a language that can be lazily unpacked to a cache user
* directory.
*
* A typical implementation of an {@code InternalResource} for a platform-specific library stored in
* the {@code META-INF/resources////} looks like this:
*
*
* @InternalResource.Id("resource-id")
* final class NativeLibResource implements InternalResource {
*
* @Override
* public void unpackFiles(Env env, Path targetDirectory) throws IOException {
* Path base = Path.of("META-INF", "resources", "mylanguage", "resource-id",
* env.getOS().toString(), env.getCPUArchitecture().toString());
* env.unpackResourceFiles(base.resolve("file-list"), targetDirectory, base);
* }
*
* @Override
* public String versionHash(Env env) {
* try {
* Path hashResource = Path.of("META-INF", "resources", "mylanguage", "resource-id",
* env.getOS().toString(), env.getCPUArchitecture().toString(), "sha256");
* return env.readResourceLines(hashResource).get(0);
* } catch (IOException ioe) {
* throw CompilerDirectives.shouldNotReachHere(ioe);
* }
* }
* }
*
*
* The resource files are listed in the
* {@code META-INF/resources/////file-list} file. For the file
* list format, refer to {@link InternalResource.Env#unpackFiles(Env, Path)}. Additionally, the
* {@code META-INF/resources/////sha256} file contains an
* SHA-256 hash of the resource files. It is recommended to use non-encapsulated resource paths that
* include the component ID and resource ID, as this helps prevent ambiguity when the language or
* instrument is used in an unnamed module.
*
* @since 23.1
*/
public interface InternalResource {
/**
* Unpacks all resources to a given target directory. The target directory is guaranteed to be
* writable and the unpacking is synchronized by a file system lock. If a resource was
* previously cached then {@link #versionHash(Env)} is invoked and the version string is
* compared. If it matches then {@link #unpackFiles(Env, Path)} will not be invoked and the
* directory will be used as previously unpacked. The target directory is guaranteed to exist
* and guaranteed to be empty.
*
* Ideally the result of this method should be idempotent in order to be safely cacheable. Care
* should be taken, if system properties are used that change the result of this method. It is
* safe to use {@link OS} and {@link CPUArchitecture} enums as internal resources are never
* cached or moved across operating system architectures.
*
* No guest language code must run as part of this method.
*
* @param targetDirectory the target directory to extract files to
* @since 23.1
*/
void unpackFiles(Env env, Path targetDirectory) throws IOException;
/**
* Returns the version hash to be used for this resource. It is the responsibility of the
* implementer to ensure that this version hash is unique. For example, an SHA-256 could be a
* good version identifier for file based resources. Since the version hash serves as a path
* component on the host filesystem, its length is restricted to a maximum of 128 bytes. If the
* version hash length exceeds this limit, an {@code IOException} will be thrown during
* unpacking.
*
* @since 23.1
*/
String versionHash(Env env);
/**
* Access to common utilities for unpacking resource files.
*
* @since 23.1
*/
final class Env {
private final Class extends InternalResource> resourceClass;
private final Module owner;
private final BooleanSupplier contextPreinitializationCheck;
Env(InternalResource resource, BooleanSupplier contextPreinitializationCheck) {
this.resourceClass = resource.getClass();
this.owner = resourceClass.getModule();
this.contextPreinitializationCheck = Objects.requireNonNull(contextPreinitializationCheck, "ContextPreinitializationCheck must be non-null.");
}
/**
* Returns {@code true} if the engine causing the resource unpacking is being
* pre-initialized.
*
* @since 23.1
*/
public boolean inContextPreinitialization() {
return contextPreinitializationCheck.getAsBoolean();
}
/**
* Returns {@code true} if resource unpacking happens during the native image build.
*
* @since 23.1
*/
@SuppressWarnings("static-method")
public boolean inNativeImageBuild() {
return TruffleOptions.AOT;
}
/**
* Returns the current processor architecture. The value can be used to resolve an
* architecture specific files during resource unpacking.
*
* @since 23.1
*/
@SuppressWarnings("static-method")
public CPUArchitecture getCPUArchitecture() {
return CPUArchitecture.getCurrent();
}
/**
* Returns the current operating system. The value can be used to resolve an OS specific
* files during resource unpacking.
*
* @since 23.1
*/
@SuppressWarnings("static-method")
public OS getOS() {
return OS.getCurrent();
}
/**
* Reads a resource from the module, which owns the {@link InternalResource} implementation
* class. If the resource is encapsulated in the module, see
* {@link Module#getResourceAsStream(String)}, the module needs to open the {@code location}
* enclosing package to the {@code org.graalvm.truffle} module. It is recommended to use
* non-encapsulated resource paths.
*
* @param location relative path that identifies the resource in the module. The relative
* path gets resolved into an absolute path using the archive root.
* @return the lines from the resource as a {@link List}
* @throws IOException in case of IO error
* @since 23.1
*/
public List readResourceLines(Path location) throws IOException {
if (location.isAbsolute()) {
throw new IllegalArgumentException("Location must be a relative path, but the absolute path " + location + " was given.");
}
try (BufferedReader in = new BufferedReader(new InputStreamReader(findResource(location), StandardCharsets.UTF_8))) {
List content = new ArrayList<>();
for (String line = in.readLine(); line != null; line = in.readLine()) {
content.add(line);
}
return content;
}
}
/**
* Extracts files from the module, which owns the {@link InternalResource} implementation
* class, listed in the {@code source} file list and places them into the {@code target}
* folder. If resources are encapsulated within the module, see
* {@link Module#getResourceAsStream(String)}, the module needs to open the enclosing
* package of the resources to the {@code org.graalvm.truffle} module. It is recommended to
* use non-encapsulated resource paths.
*
* The file list is a {@link Properties Java properties} file where resource files serve as
* keys, and the corresponding values consist of serialized attributes separated by
* {@code ','}. Currently, only the POSIX file permissions attribute is supported. The
* format of this attribute follows the same convention used by the
* {@link PosixFilePermissions#fromString(String)}.
*
* Example of a file list content:
*
*
* META-INF/resources/darwin/amd64/bin/libtrufflenfi.dylib = rwxr-xr-x
* META-INF/resources/common/include/trufflenfi.h = rw-r--r--
*
*
* @param source the relative path that identifies the file list resource in the module. The
* relative path gets resolved into an absolute path using the archive root.
* @param target the folder to extract resources to
* @param relativizeTo the path used to relativize the file list entries in the
* {@code target} folder. In other words, the file list entries are resolved
* using the {@code target} directory after removing the {@code relativizeTo}
* path.
* @throws IllegalArgumentException if {@code relativizeTo} is an absolute path or file list
* contains an absolute path
* @throws IOException in case of IO error
* @since 23.1
*/
public void unpackResourceFiles(Path source, Path target, Path relativizeTo) throws IOException {
if (source.isAbsolute()) {
throw new IllegalArgumentException("Source must be a relative path, but the absolute path " + source + " was given.");
}
if (relativizeTo.isAbsolute()) {
throw new IllegalArgumentException("RelativizeTo must be a relative path, but the absolute path " + relativizeTo + " was given.");
}
Properties fileList = loadFileList(source);
for (var e : fileList.entrySet()) {
Path resource = Path.of((String) e.getKey());
Set attrs = parseAttrs((String) e.getValue());
if (resource.isAbsolute()) {
throw new IllegalArgumentException("The file list must contain only relative paths, but the absolute path " + resource + " was given.");
}
Path relativizedPath = resource.startsWith(relativizeTo) ? relativizeTo.relativize(resource) : relativizeTo;
copyResource(resource, target.resolve(relativizedPath), attrs);
}
}
private Properties loadFileList(Path source) throws IOException {
Properties props = new Properties();
try (BufferedInputStream in = new BufferedInputStream(findResource(source))) {
props.load(in);
}
return props;
}
/**
* Parses attributes. Now a single attribute, the posix permissions, is supported. But the
* format can be extended by other attributes separated by {@code ','}.
*/
private static Set parseAttrs(String rawValue) {
String[] attrComponents = rawValue.split(",");
if (attrComponents.length != 1) {
throw new IllegalArgumentException("Invalid attributes format " + rawValue + ". Attribute value can have only a single component");
}
return PosixFilePermissions.fromString(attrComponents[0].trim());
}
private InputStream findResource(Path path) throws IOException {
String resourceName = path.toString().replace(File.separatorChar, '/');
InputStream stream;
if (owner.isNamed()) {
stream = owner.getResourceAsStream(resourceName);
} else {
Enumeration resources = resourceClass.getClassLoader().getResources(resourceName);
URL resource = preferredResource(resources);
stream = resource != null ? resource.openStream() : null;
}
if (stream == null) {
throw new NoSuchFileException(resourceName);
}
return stream;
}
private URL preferredResource(Enumeration candidates) {
if (!candidates.hasMoreElements()) {
return null; // No resource found
}
URL preferred = candidates.nextElement();
if (!candidates.hasMoreElements()) {
return preferred; // Single resource
}
CodeSource codeSource = resourceClass.getProtectionDomain().getCodeSource();
URL location = codeSource != null ? codeSource.getLocation() : null;
if (location == null) {
return preferred; // Classloader does not provide code source location, return the
// first resource
}
Path classOwner = fileURL(location);
if (classOwner == null) {
return preferred; // Code source is not a file, return the first resource
}
if (!isInClassSourceLocation(preferred, classOwner)) {
while (candidates.hasMoreElements()) {
URL candidate = candidates.nextElement();
if (isInClassSourceLocation(candidate, classOwner)) {
preferred = candidate;
break;
}
}
}
return preferred;
}
private static boolean isInClassSourceLocation(URL resource, Path classSourceLocation) {
Path resourceOwner = fileURL(resource);
return resourceOwner != null && resourceOwner.startsWith(classSourceLocation);
}
private static Path fileURL(URL url) {
try {
URI useURI = url.toURI();
if ("jar".equals(url.getProtocol())) {
String path = useURI.getRawSchemeSpecificPart();
int index = path.indexOf("!/");
String jarPath = index >= 0 ? path.substring(0, index) : null;
useURI = jarPath != null ? new URI(jarPath) : null;
}
if (useURI != null && "file".equals(useURI.getScheme())) {
return Paths.get(useURI);
}
return null;
} catch (URISyntaxException e) {
return null;
}
}
private void copyResource(Path source, Path target, Set attrs) throws IOException {
Path parent = target.getParent();
if (parent == null) {
throw CompilerDirectives.shouldNotReachHere("RelativeResourcePath must be non-empty.");
}
Files.createDirectories(parent);
try (BufferedInputStream in = new BufferedInputStream(findResource(source))) {
Files.copy(in, target);
}
if (getOS() != OS.WINDOWS) {
Files.setPosixFilePermissions(target, attrs);
}
}
}
/**
* The annotation used to lookup {@link InternalResource} by an id. The internal resource can be
* designated as either required or optional. For required internal resources, their
* availability is imperative. In addition to being annotated with this annotation, they also
* need to be registered using the {@link TruffleLanguage.Registration#internalResources()}
* method to be associated with a specific language or instrument. On the other hand, optional
* internal resources are not mandatory in the runtime. Instead of being registered using the
* {@link TruffleLanguage.Registration#internalResources()} annotation, they should supply the
* relevant language or instrument id through {@link Id#componentId()}.
*
* @since 23.1
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Id {
/**
* An internal resource identifier that is a valid path component and unique per language.
*
* @since 23.1
*/
String value();
/**
* A Truffle language or instrument identifier for which the resource is registered. The
* {@code componentId} is necessary only for optional internal resources to associate them
* with the appropriate language or instrument. For required internal resources, the
* {@code componentId} can be left unset.
*
* @since 23.1
*/
String componentId() default "";
/**
* Marks the annotated resource as optional. By default, internal resources are considered
* required and must be registered using {@code TruffleLanguage.Registration} or
* {@code TruffleInstrument.Registration}. Optional internal resources are retrieved using
* the {@link java.util.ServiceLoader} mechanism. The service implementation is generated by
* an annotation processor. Therefore, for proper functionality, the Truffle DSL processor
* must be included in the processor path.
*
* @since 23.1
*/
boolean optional() default false;
}
/**
* Represents a supported operating system.
*
* @since 23.1
*/
enum OS {
/**
* The macOS operating system.
*
* @since 23.1
*/
DARWIN("darwin"),
/**
* The Linux operating system.
*
* @since 23.1
*/
LINUX("linux"),
/**
* The Windows operating system.
*
* @since 23.1
*/
WINDOWS("windows");
private final String id;
OS(String id) {
this.id = id;
}
/**
* Returns the string representing operating system name.
*
* @since 23.1
*/
@Override
public String toString() {
return id;
}
/**
* Returns the current operating system.
*
* @since 23.1
*/
public static OS getCurrent() {
String os = System.getProperty("os.name");
if (os == null) {
throw CompilerDirectives.shouldNotReachHere("The 'os.name' system property is not set.");
} else if (os.equalsIgnoreCase("linux")) {
return LINUX;
} else if (os.equalsIgnoreCase("mac os x") || os.equalsIgnoreCase("darwin")) {
return DARWIN;
} else if (os.toLowerCase().startsWith("windows")) {
return WINDOWS;
} else {
throw CompilerDirectives.shouldNotReachHere("Unsupported OS name " + os);
}
}
}
/**
* Represents a supported CPU architecture.
*
* @since 23.1
*/
enum CPUArchitecture {
/**
* The ARMv8 64-bit architecture.
*
* @since 23.1
*/
AARCH64("aarch64"),
/**
* The x86 64-bit architecture.
*
* @since 23.1
*/
AMD64("amd64");
private final String id;
CPUArchitecture(String id) {
this.id = id;
}
/**
* Returns the string representing CPU architecture name.
*
* @since 23.1
*/
@Override
public String toString() {
return id;
}
/**
* Returns the current CPU architecture.
*
* @since 23.1
*/
public static CPUArchitecture getCurrent() {
String arch = System.getProperty("os.arch");
if (arch == null) {
throw CompilerDirectives.shouldNotReachHere("The 'os.arch' system property is not set.");
}
return switch (arch) {
case "amd64", "x86_64" -> AMD64;
case "aarch64", "arm64" -> AARCH64;
default -> throw CompilerDirectives.shouldNotReachHere("Unsupported CPU architecture " + arch);
};
}
}
}