Files
revng-revng/python/revng/project/daemon_project.py
Giacomo Vercesi f3cccf8bda revng/project: make some attributes private
Rename some attributes to make them private, as they won't be used by
the end-user.
2025-07-18 09:30:55 +02:00

182 lines
6.1 KiB
Python

#
# This file is distributed under the MIT License. See LICENSE.md for details.
#
import json
from base64 import b64decode
from typing import Dict, List, Set
import yaml
from gql import Client, gql
from gql.transport.aiohttp import AIOHTTPTransport
from revng.model import Binary, DiffSet # type: ignore[attr-defined]
from revng.pipeline_description import PipelineDescription # type: ignore[attr-defined]
from revng.pipeline_description import YamlLoader # type: ignore[attr-defined]
from .project import Project
class DaemonProject(Project):
"""
This class is used to run revng analysis and artifact through the
revng daemon via GraphQL.
"""
def __init__(self, url: str):
self.client = Client(
transport=AIOHTTPTransport(url=url),
fetch_schema_from_transport=True,
execute_timeout=None,
)
super().__init__()
self._set_model(self._get_model())
def set_binary_path(self, binary_path: str):
query = gql(
"""
mutation upload($file: Upload!) {
uploadFile(file: $file, container: "input")
}
"""
)
with open(binary_path, "rb") as binary_file:
result = self.client.execute(
query, variable_values={"file": binary_file}, upload_files=True
)
if not result["uploadFile"]:
raise RuntimeError("File upload failed")
def _get_artifact_impl(self, artifact_name: str, targets: Set[str]) -> Dict[str, bytes]:
query = gql(
"""
query($step: String!, $paths: String!, $index: BigInt!) {
produceArtifacts(step: $step, paths: $paths, index: $index) {
__typename
... on Produced {
result
}
}
}
"""
)
variables = {
"step": artifact_name,
"paths": ",".join(targets),
"index": self._get_content_commit_index(),
}
response = self.client.execute(query, variable_values=variables)
assert response["produceArtifacts"]["__typename"] == "Produced"
data: Dict[str, str] = json.loads(response["produceArtifacts"]["result"])
container_mime = self._get_artifact_container(artifact_name).MIMEType
return {
k.rsplit(":", 1)[0]: self._decode_result_on_mime(v, container_mime)
for k, v in data.items()
}
def _mapped_artifact_mime(self, artifact_name: str) -> str:
container_mime = self._get_artifact_container(artifact_name).MIMEType
if container_mime.endswith("+tar+gz"):
return container_mime.removesuffix("+tar+gz")
raise ValueError
def _analyze(
self, analysis_name: str, targets: Dict[str, List[str]] = {}, options: Dict[str, str] = {}
):
step = self._get_step_name(analysis_name)
variables = {
"step": step,
"analysis": analysis_name,
"container": json.dumps(targets),
"options": json.dumps(options),
"index": self._get_content_commit_index(),
}
query = gql(
"""
mutation($step: String!, $analysis: String!, $container: String!,
$options: String!, $index: BigInt!)
{
runAnalysis(step: $step, analysis: $analysis, options: $options,
containerToTargets: $container, index: $index)
{
__typename
}
}
"""
)
response = self.client.execute(query, variable_values=variables)
assert response["runAnalysis"]["__typename"] == "Diff"
self._set_model(self._get_model())
def _analyses_list(self, analysis_name: str):
query = gql(
"""
mutation($analysis: String!, $index: BigInt!) {
runAnalysesList(name: $analysis, index: $index) {
__typename
}
}
"""
)
variables = {"analysis": analysis_name, "index": self._get_content_commit_index()}
response = self.client.execute(query, variable_values=variables)
assert response["runAnalysesList"]["__typename"] == "Diff"
self._set_model(self._get_model())
def _commit(self):
diff = DiffSet.make(self._last_saved_model, self.model)
if len(diff.Changes) == 0:
return
diff_yaml = diff.serialize()
query = gql(
"""
mutation ($options: String!, $index: BigInt!) {
runAnalysis(step: "initial", analysis: "apply-diff", index: $index,
options: $options) {
__typename
... on Diff {
diff
}
}
}
"""
)
variables = {
"options": json.dumps(
{"apply-diff-global-name": "model.yml", "apply-diff-diff-content": diff_yaml}
),
"index": self._get_content_commit_index(),
}
response = self.client.execute(query, variable_values=variables)
assert response["runAnalysis"]["__typename"] == "Diff"
self._last_saved_model = self._get_model()
def _get_pipeline_description(self) -> PipelineDescription:
response = self.client.execute(gql("{ pipelineDescription }"))
return yaml.load(response["pipelineDescription"], Loader=YamlLoader)
def _decode_result_on_mime(self, result: str, container_mime: str) -> bytes:
if container_mime.startswith("text/") or container_mime == "image/svg":
return result.encode("utf-8")
else:
return b64decode(result)
def _get_model(self) -> Binary:
response = self.client.execute(gql("""{ getGlobal(name: "model.yml") }"""))
return Binary.deserialize(response["getGlobal"])
def _get_content_commit_index(self) -> int:
# TODO: Keep track of the commit index with websockets
response = self.client.execute(gql("{ contextCommitIndex }"))
return int(response["contextCommitIndex"])